mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-15 15:03:34 +00:00
rm
This commit is contained in:
parent
5c389efcf9
commit
bba47c28c4
10 changed files with 103 additions and 3868 deletions
1590
package-lock.json
generated
1590
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Vandee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
# pi-memory-md
|
||||
|
||||
Letta-like memory management for [pi](https://github.com/badlogic/pi-mono) using GitHub-backed markdown files.
|
||||
|
||||
## Features
|
||||
|
||||
- **Persistent Memory**: Store context, preferences, and knowledge across sessions
|
||||
- **Git-backed**: Version control with full history
|
||||
- **Prompt append**: Memory index automatically appended to conversation at session start
|
||||
- **On-demand access**: LLM reads full content via tools when needed
|
||||
- **Multi-project**: Separate memory spaces per project
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Install
|
||||
pi install npm:pi-memory-md
|
||||
# Or for latest from GitHub:
|
||||
pi install git:github.com/VandeeFeng/pi-memory-md
|
||||
|
||||
# 2. Create a GitHub repository (private recommended)
|
||||
|
||||
# 3. Configure pi
|
||||
# Add to ~/.pi/agent/settings.json:
|
||||
{
|
||||
"pi-memory-md": {
|
||||
"enabled": true,
|
||||
"repoUrl": "git@github.com:username/repo.git",
|
||||
"localPath": "~/.pi/memory-md"
|
||||
}
|
||||
}
|
||||
|
||||
# 4. Start a new pi session
|
||||
# The extension will auto-initialize and sync on first run
|
||||
```
|
||||
|
||||
**Commands available in pi:**
|
||||
|
||||
- `:memory-init` - Initialize repository structure
|
||||
- `:memory-status` - Show repository status
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Session Start
|
||||
↓
|
||||
1. Git pull (sync latest changes)
|
||||
↓
|
||||
2. Scan all .md files in memory directory
|
||||
↓
|
||||
3. Build index (descriptions + tags only - NOT full content)
|
||||
↓
|
||||
4. Append index to conversation via prompt append (not system prompt)
|
||||
↓
|
||||
5. LLM reads full file content via tools when needed
|
||||
```
|
||||
|
||||
**Why index-only via prompt append?** Keeps token usage low while making full content accessible on-demand. The index is appended to the conversation, not injected into the system prompt.
|
||||
|
||||
## Available Tools
|
||||
|
||||
The LLM can use these tools to interact with memory:
|
||||
|
||||
| Tool | Parameters | Description |
|
||||
| --------------- | ------------------------------------- | ------------------------------------- | ---------- | -------------- |
|
||||
| `memory_init` | `{force?: boolean}` | Initialize or reinitialize repository |
|
||||
| `memory_sync` | `{action: "pull" | "push" | "status"}` | Git operations |
|
||||
| `memory_read` | `{path: string}` | Read a memory file |
|
||||
| `memory_write` | `{path, content, description, tags?}` | Create/update memory file |
|
||||
| `memory_list` | `{directory?: string}` | List all memory files |
|
||||
| `memory_search` | `{query, searchIn}` | Search by content/tags/description |
|
||||
|
||||
## Memory File Format
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: "User identity and background"
|
||||
tags: ["user", "identity"]
|
||||
created: "2026-02-14"
|
||||
updated: "2026-02-14"
|
||||
---
|
||||
|
||||
# Your Content Here
|
||||
|
||||
Markdown content...
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
~/.pi/memory-md/
|
||||
└── project-name/
|
||||
├── core/
|
||||
│ ├── user/ # Your preferences
|
||||
│ │ ├── identity.md
|
||||
│ │ └── prefer.md
|
||||
│ └── project/ # Project context
|
||||
│ └── tech-stack.md
|
||||
└── reference/ # On-demand docs
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"pi-memory-md": {
|
||||
"enabled": true,
|
||||
"repoUrl": "git@github.com:username/repo.git",
|
||||
"localPath": "~/.pi/memory-md",
|
||||
"injection": "message-append",
|
||||
"autoSync": {
|
||||
"onSessionStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Setting | Default | Description |
|
||||
| ------------------------- | ------------------ | -------------------------------------------------------------- |
|
||||
| `enabled` | `true` | Enable extension |
|
||||
| `repoUrl` | Required | GitHub repository URL |
|
||||
| `localPath` | `~/.pi/memory-md` | Local clone path |
|
||||
| `injection` | `"message-append"` | Memory injection mode: `"message-append"` or `"system-prompt"` |
|
||||
| `autoSync.onSessionStart` | `true` | Git pull on session start |
|
||||
|
||||
### Memory Injection Modes
|
||||
|
||||
The extension supports two modes for injecting memory into the conversation:
|
||||
|
||||
#### 1. Message Append (Default)
|
||||
|
||||
```json
|
||||
{
|
||||
"pi-memory-md": {
|
||||
"injection": "message-append"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Memory is sent as a custom message before the user's first message
|
||||
- Not visible in the TUI (`display: false`)
|
||||
- Persists in the session history
|
||||
- Injected only once per session (on first agent turn)
|
||||
- **Pros**: Lower token usage, memory persists naturally in conversation
|
||||
- **Cons**: Only visible when the model scrolls back to earlier messages
|
||||
|
||||
#### 2. System Prompt
|
||||
|
||||
```json
|
||||
{
|
||||
"pi-memory-md": {
|
||||
"injection": "system-prompt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Memory is appended to the system prompt
|
||||
- Rebuilt and injected on every agent turn
|
||||
- Always visible to the model in the system context
|
||||
- **Pros**: Memory always present in system context, no need to scroll back
|
||||
- **Cons**: Higher token usage (repeated on every prompt)
|
||||
|
||||
**Recommendation**: Use `message-append` (default) for optimal token efficiency. Switch to `system-prompt` if you notice the model not accessing memory consistently.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
Simply talk to pi - the LLM will automatically use memory tools when appropriate:
|
||||
|
||||
```
|
||||
You: Save my preference for 2-space indentation in TypeScript files to memory.
|
||||
|
||||
Pi: [Uses memory_write tool to save your preference]
|
||||
```
|
||||
|
||||
You can also explicitly request operations:
|
||||
|
||||
```
|
||||
You: List all memory files for this project.
|
||||
You: Search memory for "typescript" preferences.
|
||||
You: Read core/user/identity.md
|
||||
You: Sync my changes to the repository.
|
||||
```
|
||||
|
||||
The LLM automatically:
|
||||
|
||||
- Reads memory index at session start (appended to conversation)
|
||||
- Writes new information when you ask to remember something
|
||||
- Syncs changes when needed
|
||||
|
||||
## Commands
|
||||
|
||||
Use these directly in pi:
|
||||
|
||||
- `:memory-status` - Show repository status
|
||||
- `:memory-init` - Initialize repository structure
|
||||
|
||||
## Reference
|
||||
|
||||
- [Introducing Context Repositories: Git-based Memory for Coding Agents | Letta](https://www.letta.com/blog/context-repositories)
|
||||
|
|
@ -1,641 +0,0 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
ExtensionContext,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import type { GrayMatterFile } from "gray-matter";
|
||||
import matter from "gray-matter";
|
||||
import { registerAllTools } from "./tools.js";
|
||||
|
||||
/**
|
||||
* Type definitions for memory files, settings, and git operations.
|
||||
*/
|
||||
|
||||
export interface MemoryFrontmatter {
|
||||
description: string;
|
||||
limit?: number;
|
||||
tags?: string[];
|
||||
created?: string;
|
||||
updated?: string;
|
||||
}
|
||||
|
||||
export interface MemoryFile {
|
||||
path: string;
|
||||
frontmatter: MemoryFrontmatter;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface MemoryMdSettings {
|
||||
enabled?: boolean;
|
||||
repoUrl?: string;
|
||||
localPath?: string;
|
||||
autoSync?: {
|
||||
onSessionStart?: boolean;
|
||||
};
|
||||
injection?: "system-prompt" | "message-append";
|
||||
systemPrompt?: {
|
||||
maxTokens?: number;
|
||||
includeProjects?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitResult {
|
||||
stdout: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
updated?: boolean;
|
||||
}
|
||||
|
||||
export type ParsedFrontmatter = GrayMatterFile<string>["data"];
|
||||
|
||||
/**
|
||||
* Helper functions for paths, dates, and settings.
|
||||
*/
|
||||
|
||||
const DEFAULT_LOCAL_PATH = path.join(os.homedir(), ".pi", "memory-md");
|
||||
|
||||
export function getCurrentDate(): string {
|
||||
return new Date().toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
function expandPath(p: string): string {
|
||||
if (p.startsWith("~")) {
|
||||
return path.join(os.homedir(), p.slice(1));
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
export function getMemoryDir(
|
||||
settings: MemoryMdSettings,
|
||||
ctx: ExtensionContext,
|
||||
): string {
|
||||
const basePath = settings.localPath || DEFAULT_LOCAL_PATH;
|
||||
return path.join(basePath, path.basename(ctx.cwd));
|
||||
}
|
||||
|
||||
function getRepoName(settings: MemoryMdSettings): string {
|
||||
if (!settings.repoUrl) return "memory-md";
|
||||
const match = settings.repoUrl.match(/\/([^/]+?)(\.git)?$/);
|
||||
return match ? match[1] : "memory-md";
|
||||
}
|
||||
|
||||
function loadSettings(): MemoryMdSettings {
|
||||
const DEFAULT_SETTINGS: MemoryMdSettings = {
|
||||
enabled: true,
|
||||
repoUrl: "",
|
||||
localPath: DEFAULT_LOCAL_PATH,
|
||||
autoSync: { onSessionStart: true },
|
||||
injection: "message-append",
|
||||
systemPrompt: {
|
||||
maxTokens: 10000,
|
||||
includeProjects: ["current"],
|
||||
},
|
||||
};
|
||||
|
||||
const globalSettings = path.join(
|
||||
os.homedir(),
|
||||
".pi",
|
||||
"agent",
|
||||
"settings.json",
|
||||
);
|
||||
if (!fs.existsSync(globalSettings)) {
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(globalSettings, "utf-8");
|
||||
const parsed = JSON.parse(content);
|
||||
const loadedSettings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...(parsed["pi-memory-md"] as MemoryMdSettings),
|
||||
};
|
||||
|
||||
if (loadedSettings.localPath) {
|
||||
loadedSettings.localPath = expandPath(loadedSettings.localPath);
|
||||
}
|
||||
|
||||
return loadedSettings;
|
||||
} catch (error) {
|
||||
console.warn("Failed to load memory settings:", error);
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Git sync operations (fetch, pull, push, status).
|
||||
*/
|
||||
|
||||
export async function gitExec(
|
||||
pi: ExtensionAPI,
|
||||
cwd: string,
|
||||
...args: string[]
|
||||
): Promise<GitResult> {
|
||||
try {
|
||||
const result = await pi.exec("git", args, { cwd });
|
||||
return {
|
||||
stdout: result.stdout || "",
|
||||
success: true,
|
||||
};
|
||||
} catch {
|
||||
return { stdout: "", success: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncRepository(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
isRepoInitialized: { value: boolean },
|
||||
): Promise<SyncResult> {
|
||||
const localPath = settings.localPath;
|
||||
const repoUrl = settings.repoUrl;
|
||||
|
||||
if (!repoUrl || !localPath) {
|
||||
return {
|
||||
success: false,
|
||||
message: "GitHub repo URL or local path not configured",
|
||||
};
|
||||
}
|
||||
|
||||
if (fs.existsSync(localPath)) {
|
||||
const gitDir = path.join(localPath, ".git");
|
||||
if (!fs.existsSync(gitDir)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Directory exists but is not a git repo: ${localPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
const pullResult = await gitExec(
|
||||
pi,
|
||||
localPath,
|
||||
"pull",
|
||||
"--rebase",
|
||||
"--autostash",
|
||||
);
|
||||
if (!pullResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Pull failed - try manual git operations",
|
||||
};
|
||||
}
|
||||
|
||||
isRepoInitialized.value = true;
|
||||
const updated =
|
||||
pullResult.stdout.includes("Updating") ||
|
||||
pullResult.stdout.includes("Fast-forward");
|
||||
const repoName = getRepoName(settings);
|
||||
return {
|
||||
success: true,
|
||||
message: updated
|
||||
? `Pulled latest changes from [${repoName}]`
|
||||
: `[${repoName}] is already latest`,
|
||||
updated,
|
||||
};
|
||||
}
|
||||
|
||||
fs.mkdirSync(localPath, { recursive: true });
|
||||
|
||||
const memoryDirName = path.basename(localPath);
|
||||
const parentDir = path.dirname(localPath);
|
||||
const cloneResult = await gitExec(
|
||||
pi,
|
||||
parentDir,
|
||||
"clone",
|
||||
repoUrl,
|
||||
memoryDirName,
|
||||
);
|
||||
|
||||
if (cloneResult.success) {
|
||||
isRepoInitialized.value = true;
|
||||
const repoName = getRepoName(settings);
|
||||
return {
|
||||
success: true,
|
||||
message: `Cloned [${repoName}] successfully`,
|
||||
updated: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, message: "Clone failed - check repo URL and auth" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory file read/write/list operations.
|
||||
*/
|
||||
|
||||
function validateFrontmatter(data: ParsedFrontmatter): {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
} {
|
||||
if (!data) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "No frontmatter found (requires --- delimiters)",
|
||||
};
|
||||
}
|
||||
|
||||
const frontmatter = data as MemoryFrontmatter;
|
||||
|
||||
if (!frontmatter.description || typeof frontmatter.description !== "string") {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Frontmatter must have a 'description' field (string)",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
frontmatter.limit !== undefined &&
|
||||
(typeof frontmatter.limit !== "number" || frontmatter.limit <= 0)
|
||||
) {
|
||||
return { valid: false, error: "'limit' must be a positive number" };
|
||||
}
|
||||
|
||||
if (frontmatter.tags !== undefined && !Array.isArray(frontmatter.tags)) {
|
||||
return { valid: false, error: "'tags' must be an array of strings" };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
export function readMemoryFile(filePath: string): MemoryFile | null {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const parsed = matter(content);
|
||||
const validation = validateFrontmatter(parsed.data);
|
||||
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error);
|
||||
}
|
||||
|
||||
return {
|
||||
path: filePath,
|
||||
frontmatter: parsed.data as MemoryFrontmatter,
|
||||
content: parsed.content,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to read memory file ${filePath}:`,
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function listMemoryFiles(memoryDir: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
function walkDir(dir: string) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(fullPath);
|
||||
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkDir(memoryDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
export function writeMemoryFile(
|
||||
filePath: string,
|
||||
content: string,
|
||||
frontmatter: MemoryFrontmatter,
|
||||
): void {
|
||||
const fileDir = path.dirname(filePath);
|
||||
fs.mkdirSync(fileDir, { recursive: true });
|
||||
const frontmatterStr = matter.stringify(content, frontmatter);
|
||||
fs.writeFileSync(filePath, frontmatterStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build memory context for agent prompt.
|
||||
*/
|
||||
|
||||
function ensureDirectoryStructure(memoryDir: string): void {
|
||||
const dirs = [
|
||||
path.join(memoryDir, "core", "user"),
|
||||
path.join(memoryDir, "core", "project"),
|
||||
path.join(memoryDir, "reference"),
|
||||
];
|
||||
|
||||
for (const dir of dirs) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function createDefaultFiles(memoryDir: string): void {
|
||||
const identityFile = path.join(memoryDir, "core", "user", "identity.md");
|
||||
if (!fs.existsSync(identityFile)) {
|
||||
writeMemoryFile(
|
||||
identityFile,
|
||||
"# User Identity\n\nCustomize this file with your information.",
|
||||
{
|
||||
description: "User identity and background",
|
||||
tags: ["user", "identity"],
|
||||
created: getCurrentDate(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const preferFile = path.join(memoryDir, "core", "user", "prefer.md");
|
||||
if (!fs.existsSync(preferFile)) {
|
||||
writeMemoryFile(
|
||||
preferFile,
|
||||
"# User Preferences\n\n## Communication Style\n- Be concise\n- Show code examples\n\n## Code Style\n- 2 space indentation\n- Prefer const over var\n- Functional programming preferred",
|
||||
{
|
||||
description: "User habits and code style preferences",
|
||||
tags: ["user", "preferences"],
|
||||
created: getCurrentDate(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildMemoryContext(
|
||||
settings: MemoryMdSettings,
|
||||
ctx: ExtensionContext,
|
||||
): string {
|
||||
const coreDir = path.join(getMemoryDir(settings, ctx), "core");
|
||||
|
||||
if (!fs.existsSync(coreDir)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const files = listMemoryFiles(coreDir);
|
||||
if (files.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const lines: string[] = [
|
||||
"# Project Memory",
|
||||
"",
|
||||
"Available memory files (use memory_read to view full content):",
|
||||
"",
|
||||
];
|
||||
|
||||
for (const filePath of files) {
|
||||
const memory = readMemoryFile(filePath);
|
||||
if (memory) {
|
||||
const relPath = path.relative(memoryDir, filePath);
|
||||
const { description, tags } = memory.frontmatter;
|
||||
const tagStr = tags?.join(", ") || "none";
|
||||
lines.push(`- ${relPath}`);
|
||||
lines.push(` Description: ${description}`);
|
||||
lines.push(` Tags: ${tagStr}`);
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Main extension initialization.
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. session_start: Start async sync (non-blocking), build memory context
|
||||
* 2. before_agent_start: Wait for sync, then inject memory on first agent turn
|
||||
* 3. Register tools and commands for memory operations
|
||||
*
|
||||
* Memory injection modes:
|
||||
* - message-append (default): Send as custom message with display: false, not visible in TUI but persists in session
|
||||
* - system-prompt: Append to system prompt on each agent turn (rebuilds every prompt)
|
||||
*
|
||||
* Key optimization:
|
||||
* - Sync runs asynchronously without blocking user input
|
||||
* - Memory is injected after user sends first message (before_agent_start)
|
||||
*
|
||||
* Configuration:
|
||||
* Set injection in settings to choose between "message-append" or "system-prompt"
|
||||
*
|
||||
* Commands:
|
||||
* - /memory-status: Show repository status
|
||||
* - /memory-init: Initialize memory repository
|
||||
* - /memory-refresh: Manually refresh memory context
|
||||
*/
|
||||
|
||||
export default function memoryMdExtension(pi: ExtensionAPI) {
|
||||
let settings: MemoryMdSettings = loadSettings();
|
||||
const repoInitialized = { value: false };
|
||||
let syncPromise: Promise<SyncResult> | null = null;
|
||||
let cachedMemoryContext: string | null = null;
|
||||
let memoryInjected = false;
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
settings = loadSettings();
|
||||
|
||||
if (!settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const coreDir = path.join(memoryDir, "core");
|
||||
|
||||
if (!fs.existsSync(coreDir)) {
|
||||
ctx.ui.notify(
|
||||
"Memory-md not initialized. Use /memory-init to set up project memory.",
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.autoSync?.onSessionStart && settings.localPath) {
|
||||
syncPromise = syncRepository(pi, settings, repoInitialized).then(
|
||||
(syncResult) => {
|
||||
if (settings.repoUrl) {
|
||||
ctx.ui.notify(
|
||||
syncResult.message,
|
||||
syncResult.success ? "info" : "error",
|
||||
);
|
||||
}
|
||||
return syncResult;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
cachedMemoryContext = buildMemoryContext(settings, ctx);
|
||||
memoryInjected = false;
|
||||
});
|
||||
|
||||
pi.on("before_agent_start", async (event, ctx) => {
|
||||
if (syncPromise) {
|
||||
await syncPromise;
|
||||
syncPromise = null;
|
||||
}
|
||||
|
||||
if (!cachedMemoryContext) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mode = settings.injection || "message-append";
|
||||
const isFirstInjection = !memoryInjected;
|
||||
|
||||
if (isFirstInjection) {
|
||||
memoryInjected = true;
|
||||
const fileCount = cachedMemoryContext
|
||||
.split("\n")
|
||||
.filter((l) => l.startsWith("-")).length;
|
||||
ctx.ui.notify(`Memory injected: ${fileCount} files (${mode})`, "info");
|
||||
}
|
||||
|
||||
if (mode === "message-append" && isFirstInjection) {
|
||||
return {
|
||||
message: {
|
||||
customType: "pi-memory-md",
|
||||
content: `# Project Memory\n\n${cachedMemoryContext}`,
|
||||
display: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === "system-prompt") {
|
||||
return {
|
||||
systemPrompt: `${event.systemPrompt}\n\n# Project Memory\n\n${cachedMemoryContext}`,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
registerAllTools(pi, settings, repoInitialized);
|
||||
|
||||
pi.registerCommand("memory-status", {
|
||||
description: "Show memory repository status",
|
||||
handler: async (_args, ctx) => {
|
||||
const projectName = path.basename(ctx.cwd);
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const coreUserDir = path.join(memoryDir, "core", "user");
|
||||
|
||||
if (!fs.existsSync(coreUserDir)) {
|
||||
ctx.ui.notify(
|
||||
`Memory: ${projectName} | Not initialized | Use /memory-init to set up`,
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await gitExec(
|
||||
pi,
|
||||
settings.localPath!,
|
||||
"status",
|
||||
"--porcelain",
|
||||
);
|
||||
const isDirty = result.stdout.trim().length > 0;
|
||||
|
||||
ctx.ui.notify(
|
||||
`Memory: ${projectName} | Repo: ${isDirty ? "Uncommitted changes" : "Clean"} | Path: ${memoryDir}`,
|
||||
isDirty ? "warning" : "info",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("memory-init", {
|
||||
description: "Initialize memory repository",
|
||||
handler: async (_args, ctx) => {
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const alreadyInitialized = fs.existsSync(
|
||||
path.join(memoryDir, "core", "user"),
|
||||
);
|
||||
|
||||
const result = await syncRepository(pi, settings, repoInitialized);
|
||||
|
||||
if (!result.success) {
|
||||
ctx.ui.notify(`Initialization failed: ${result.message}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
ensureDirectoryStructure(memoryDir);
|
||||
createDefaultFiles(memoryDir);
|
||||
|
||||
if (alreadyInitialized) {
|
||||
ctx.ui.notify(`Memory already exists: ${result.message}`, "info");
|
||||
} else {
|
||||
ctx.ui.notify(
|
||||
`Memory initialized: ${result.message}\n\nCreated:\n - core/user\n - core/project\n - reference`,
|
||||
"info",
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("memory-refresh", {
|
||||
description: "Refresh memory context from files",
|
||||
handler: async (_args, ctx) => {
|
||||
const memoryContext = buildMemoryContext(settings, ctx);
|
||||
|
||||
if (!memoryContext) {
|
||||
ctx.ui.notify("No memory files found to refresh", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
cachedMemoryContext = memoryContext;
|
||||
memoryInjected = false;
|
||||
|
||||
const mode = settings.injection || "message-append";
|
||||
const fileCount = memoryContext
|
||||
.split("\n")
|
||||
.filter((l) => l.startsWith("-")).length;
|
||||
|
||||
if (mode === "message-append") {
|
||||
pi.sendMessage({
|
||||
customType: "pi-memory-md-refresh",
|
||||
content: `# Project Memory (Refreshed)\n\n${memoryContext}`,
|
||||
display: false,
|
||||
});
|
||||
ctx.ui.notify(
|
||||
`Memory refreshed: ${fileCount} files injected (${mode})`,
|
||||
"info",
|
||||
);
|
||||
} else {
|
||||
ctx.ui.notify(
|
||||
`Memory cache refreshed: ${fileCount} files (will be injected on next prompt)`,
|
||||
"info",
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("memory-check", {
|
||||
description: "Check memory folder structure",
|
||||
handler: async (_args, ctx) => {
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
|
||||
if (!fs.existsSync(memoryDir)) {
|
||||
ctx.ui.notify(`Memory directory not found: ${memoryDir}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const { execSync } = await import("node:child_process");
|
||||
let treeOutput = "";
|
||||
|
||||
try {
|
||||
treeOutput = execSync(`tree -L 3 -I "node_modules" "${memoryDir}"`, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
} catch {
|
||||
try {
|
||||
treeOutput = execSync(
|
||||
`find "${memoryDir}" -type d -not -path "*/node_modules/*"`,
|
||||
{ encoding: "utf-8" },
|
||||
);
|
||||
} catch {
|
||||
treeOutput = "Unable to generate directory tree.";
|
||||
}
|
||||
}
|
||||
|
||||
ctx.ui.notify(treeOutput.trim(), "info");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
{
|
||||
"name": "pi-memory-md",
|
||||
"version": "0.1.1",
|
||||
"description": "Letta-like memory management for pi using structured markdown files in a GitHub repository",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"author": "VandeePunk",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/VandeeFeng/pi-memory-md.git"
|
||||
},
|
||||
"keywords": [
|
||||
"pi-package",
|
||||
"pi-extension",
|
||||
"pi-skill",
|
||||
"memory",
|
||||
"markdown",
|
||||
"git",
|
||||
"letta",
|
||||
"persistent-memory",
|
||||
"ai-memory",
|
||||
"coding-agent"
|
||||
],
|
||||
"dependencies": {
|
||||
"gray-matter": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mariozechner/pi-coding-agent": "latest",
|
||||
"@types/node": "^20.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"pi": {
|
||||
"extensions": [
|
||||
"./memory-md.ts"
|
||||
],
|
||||
"skills": [
|
||||
"./skills/memory-init/SKILL.md",
|
||||
"./skills/memory-management/SKILL.md",
|
||||
"./skills/memory-sync/SKILL.md",
|
||||
"./skills/memory-search/SKILL.md"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"memory-md.ts",
|
||||
"tools.ts",
|
||||
"skills",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"check": "biome check --write --error-on-warnings . && tsgo --noEmit"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
---
|
||||
name: memory-init
|
||||
description: Initial setup and bootstrap for pi-memory-md repository
|
||||
---
|
||||
|
||||
# Memory Init
|
||||
|
||||
Use this skill to set up pi-memory-md for the first time or reinitialize an existing installation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **GitHub repository** - Create a new empty repository on GitHub
|
||||
2. **Git access** - Configure SSH keys or personal access token
|
||||
3. **Node.js & npm** - For installing the package
|
||||
|
||||
## Step 1: Install Package
|
||||
|
||||
```bash
|
||||
pi install npm:pi-memory-md
|
||||
```
|
||||
|
||||
## Step 2: Create GitHub Repository
|
||||
|
||||
Create a new repository on GitHub:
|
||||
|
||||
- Name it something like `memory-md` or `pi-memory`
|
||||
- Make it private (recommended)
|
||||
- Don't initialize with README (we'll do that)
|
||||
|
||||
**Clone URL will be:** `git@github.com:username/repo-name.git`
|
||||
|
||||
## Step 3: Configure Settings
|
||||
|
||||
Add to your settings file (global: `~/.pi/agent/settings.json`, project: `.pi/settings.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"pi-memory-md": {
|
||||
"enabled": true,
|
||||
"repoUrl": "git@github.com:username/repo-name.git",
|
||||
"localPath": "~/.pi/memory-md",
|
||||
"autoSync": {
|
||||
"onSessionStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Settings explained:**
|
||||
|
||||
| Setting | Purpose | Default |
|
||||
| ------------------------- | ----------------------------------- | ----------------- |
|
||||
| `enabled` | Enable/disable extension | `true` |
|
||||
| `repoUrl` | GitHub repository URL | Required |
|
||||
| `localPath` | Local clone location (supports `~`) | `~/.pi/memory-md` |
|
||||
| `autoSync.onSessionStart` | Auto-pull on session start | `true` |
|
||||
|
||||
## Step 4: Initialize Repository
|
||||
|
||||
Start pi and run:
|
||||
|
||||
```
|
||||
memory_init()
|
||||
```
|
||||
|
||||
**This does:**
|
||||
|
||||
1. Clones the GitHub repository
|
||||
2. Creates directory structure:
|
||||
- `core/user/` - Your identity and preferences
|
||||
- `core/project/` - Project-specific info
|
||||
3. Creates default files:
|
||||
- `core/user/identity.md` - User identity template
|
||||
- `core/user/prefer.md` - User preferences template
|
||||
|
||||
**Example output:**
|
||||
|
||||
```
|
||||
Memory repository initialized:
|
||||
Cloned repository successfully
|
||||
|
||||
Created directory structure:
|
||||
- core/user
|
||||
- core/project
|
||||
- reference
|
||||
```
|
||||
|
||||
## Step 5: Import Preferences from AGENTS.md
|
||||
|
||||
After initialization, extract relevant preferences from your `AGENTS.md` file to populate `prefer.md`:
|
||||
|
||||
1. **Read AGENTS.md** (typically at `.pi/agent/AGENTS.md` or project root)
|
||||
|
||||
2. **Extract relevant sections** such as:
|
||||
- IMPORTANT Rules
|
||||
- Code Quality Principles
|
||||
- Coding Style Preferences
|
||||
- Architecture Principles
|
||||
- Development Workflow
|
||||
- Technical Preferences
|
||||
|
||||
3. **Present extracted content** to the user in a summarized format
|
||||
|
||||
4. **Ask first confirmation**: Include these extracted preferences in `prefer.md`?
|
||||
|
||||
```
|
||||
Found these preferences in AGENTS.md:
|
||||
- IMPORTANT Rules: [summary]
|
||||
- Code Quality Principles: [summary]
|
||||
- Coding Style: [summary]
|
||||
|
||||
Include these in core/user/prefer.md? (yes/no)
|
||||
```
|
||||
|
||||
5. **Ask for additional content**: Is there anything else you want to add to your preferences?
|
||||
|
||||
```
|
||||
Any additional preferences you'd like to include? (e.g., communication style, specific tools, workflows)
|
||||
```
|
||||
|
||||
6. **Update prefer.md** with:
|
||||
- Extracted content from AGENTS.md (if user confirmed)
|
||||
- Any additional preferences provided by user
|
||||
|
||||
## Step 6: Verify Setup
|
||||
|
||||
Check status with command:
|
||||
|
||||
```
|
||||
/memory-status
|
||||
```
|
||||
|
||||
Should show: `Memory: project-name | Repo: Clean | Path: {localPath}/project-name`
|
||||
|
||||
List files:
|
||||
|
||||
```
|
||||
memory_list()
|
||||
```
|
||||
|
||||
Should show: `core/user/identity.md`, `core/user/prefer.md`
|
||||
|
||||
## Project Structure
|
||||
|
||||
**Base path**: Configured via `settings["pi-memory-md"].localPath` (default: `~/.pi/memory-md`)
|
||||
|
||||
Each project gets its own folder in the repository:
|
||||
|
||||
```
|
||||
{localPath}/
|
||||
├── project-a/
|
||||
│ ├── core/
|
||||
│ │ ├── user/
|
||||
│ │ │ ├── identity.md
|
||||
│ │ │ └── prefer.md
|
||||
│ │ └── project/
|
||||
│ └── reference/
|
||||
├── project-b/
|
||||
│ └── ...
|
||||
└── project-c/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Project name is derived from:
|
||||
|
||||
- Git repository name (if in a git repo)
|
||||
- Or current directory name
|
||||
|
||||
## First-Time Setup Script
|
||||
|
||||
Automate setup with this script:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# setup-memory-md.sh
|
||||
|
||||
REPO_URL="git@github.com:username/memory-repo.git"
|
||||
SETTINGS_FILE="$HOME/.pi/agent/settings.json"
|
||||
|
||||
# Backup existing settings
|
||||
cp "$SETTINGS_FILE" "$SETTINGS_FILE.bak"
|
||||
|
||||
# Add pi-memory-md configuration
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const settingsPath = '$SETTINGS_FILE';
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
settings['pi-memory-md'] = {
|
||||
enabled: true,
|
||||
repoUrl: '$REPO_URL',
|
||||
localPath: path.join(require('os').homedir(), '.pi', 'memory-md'),
|
||||
autoSync: {
|
||||
onSessionStart: true,
|
||||
onMessageCreate: false
|
||||
}
|
||||
};
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
||||
"
|
||||
|
||||
echo "Settings configured. Now run: memory_init()"
|
||||
```
|
||||
|
||||
## Reinitializing
|
||||
|
||||
To reset everything:
|
||||
|
||||
```
|
||||
memory_init(force=true)
|
||||
```
|
||||
|
||||
**Warning:** This will re-clone the repository, potentially losing local uncommitted changes.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Clone Failed
|
||||
|
||||
**Error:** `Clone failed: Permission denied`
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Verify SSH keys are configured: `ssh -T git@github.com`
|
||||
2. Check repo URL is correct in settings
|
||||
3. Ensure repo exists on GitHub
|
||||
|
||||
### Settings Not Found
|
||||
|
||||
**Error:** `GitHub repo URL not configured in settings["pi-memory-md"].repoUrl`
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Edit settings file (global or project)
|
||||
2. Add `pi-memory-md` section (see Step 3)
|
||||
3. Run `/reload` in pi
|
||||
|
||||
### Directory Already Exists
|
||||
|
||||
**Error:** `Directory exists but is not a git repo`
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Remove existing directory: `rm -rf {localPath}` (use your configured path)
|
||||
2. Run `memory_init()` again
|
||||
|
||||
### No Write Permission
|
||||
|
||||
**Error:** `EACCES: permission denied`
|
||||
|
||||
**Solution:**
|
||||
|
||||
1. Check directory permissions: `ls -la {localPath}/..` (use your configured path)
|
||||
2. Fix ownership: `sudo chown -R $USER:$USER {localPath}` (use your configured path)
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After setup, verify:
|
||||
|
||||
- [ ] Package installed: `pi install npm:pi-memory-md`
|
||||
- [ ] Settings configured in settings file
|
||||
- [ ] GitHub repository exists and is accessible
|
||||
- [ ] Repository cloned to configured `localPath`
|
||||
- [ ] Directory structure created
|
||||
- [ ] `/memory-status` shows correct info
|
||||
- [ ] `memory_list()` returns files
|
||||
- [ ] `prefer.md` populated (either from AGENTS.md or default template)
|
||||
|
||||
## Next Steps
|
||||
|
||||
After initialization:
|
||||
|
||||
1. **Import preferences** - Agent will prompt to extract from AGENTS.md
|
||||
2. Edit your identity: `memory_read(path="core/user/identity.md")` then `memory_write(...)` to update
|
||||
3. Review preferences: `memory_read(path="core/user/prefer.md")`
|
||||
4. Add project context: `memory_write(path="core/project/overview.md", ...)`
|
||||
5. Learn more: See `memory-management` skill
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `memory-management` - Creating and managing memory files
|
||||
- `memory-sync` - Git synchronization
|
||||
- `memory-search` - Finding information
|
||||
|
|
@ -1,308 +0,0 @@
|
|||
---
|
||||
name: memory-management
|
||||
description: Core memory operations for pi-memory-md - create, read, update, and delete memory files
|
||||
---
|
||||
|
||||
# Memory Management
|
||||
|
||||
Use this skill when working with pi-memory-md memory files. Memory is stored as markdown files with YAML frontmatter in a git repository.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Inspired by Letta memory filesystem:
|
||||
|
||||
- **File-based memory**: Each memory is a `.md` file with YAML frontmatter
|
||||
- **Git-backed**: Full version control and cross-device sync
|
||||
- **Auto-injection**: Files in `core/` are automatically injected to context
|
||||
- **Organized by purpose**: Fixed structure for core info, flexible for everything else
|
||||
|
||||
## Directory Structure
|
||||
|
||||
**Base path**: Configured via `settings["pi-memory-md"].localPath` (default: `~/.pi/memory-md`)
|
||||
|
||||
```
|
||||
{localPath}/
|
||||
└── {project-name}/ # Project memory root
|
||||
├── core/ # Auto-injected to context every session
|
||||
│ ├── user/ # 【FIXED】User information
|
||||
│ │ ├── identity.md # Who the user is
|
||||
│ │ └── prefer.md # User habits and code style preferences
|
||||
│ │
|
||||
│ └── project/ # 【FIXED】Project information (pre-created)
|
||||
│ ├── overview.md # Project overview
|
||||
│ ├── architecture.md # Architecture and design
|
||||
│ ├── conventions.md # Code conventions
|
||||
│ └── commands.md # Common commands
|
||||
│
|
||||
├── docs/ # 【AGENT-CREATED】Reference documentation
|
||||
├── archive/ # 【AGENT-CREATED】Historical information
|
||||
├── research/ # 【AGENT-CREATED】Research findings
|
||||
└── notes/ # 【AGENT-CREATED】Standalone notes
|
||||
```
|
||||
|
||||
**Important:** `core/project/` is a pre-defined folder under `core/`. Do NOT create another `project/` folder at the project root level.
|
||||
|
||||
## Core Design: Fixed vs Flexible
|
||||
|
||||
### 【FIXED】core/user/ and core/project/
|
||||
|
||||
These are **pre-defined** and **auto-injected** into every session:
|
||||
|
||||
**core/user/** - User information (2 fixed files)
|
||||
|
||||
- `identity.md` - Who the user is (name, role, background)
|
||||
- `prefer.md` - User habits and code style preferences
|
||||
|
||||
**core/project/** - Project information
|
||||
|
||||
- `overview.md` - Project overview
|
||||
- `architecture.md` - Architecture and design
|
||||
- `conventions.md` - Code conventions
|
||||
- `commands.md` - Common commands
|
||||
- `changelog.md` - Development history
|
||||
|
||||
**Why fixed?**
|
||||
|
||||
- Always in context, no need to remember to load
|
||||
- Core identity that defines every interaction
|
||||
- Project context needed for all decisions
|
||||
|
||||
**Rule:** ONLY `user/` and `project/` exist under `core/`. No other folders.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
### Does this need to be in EVERY conversation?
|
||||
|
||||
**Yes** → Place under `core/`
|
||||
|
||||
- User-related → `core/user/`
|
||||
- Project-related → `core/project/`
|
||||
|
||||
**No** → Place at project root level (same level as `core/`)
|
||||
|
||||
- Reference docs → `docs/`
|
||||
- Historical → `archive/`
|
||||
- Research → `research/`
|
||||
- Notes → `notes/`
|
||||
- Other? → Create appropriate folder
|
||||
|
||||
**Important:** `core/project/` is a FIXED subdirectory under `core/`. Always use `core/project/` for project-specific memory files, NEVER create a `project/` folder at the root level.
|
||||
|
||||
## YAML Frontmatter Schema
|
||||
|
||||
Every memory file MUST have YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: "Human-readable description of this memory file"
|
||||
tags: ["user", "identity"]
|
||||
created: "2026-02-14"
|
||||
updated: "2026-02-14"
|
||||
---
|
||||
```
|
||||
|
||||
**Required fields:**
|
||||
|
||||
- `description` (string) - Human-readable description
|
||||
|
||||
**Optional fields:**
|
||||
|
||||
- `tags` (array of strings) - For searching and categorization
|
||||
- `created` (date) - File creation date (auto-added on create)
|
||||
- `updated` (date) - Last modification date (auto-updated on update)
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: User Identity (core/user/identity.md)
|
||||
|
||||
```bash
|
||||
memory_write(
|
||||
path="core/user/identity.md",
|
||||
description="User identity and background",
|
||||
tags=["user", "identity"],
|
||||
content="# User Identity\n\nName: Vandee\nRole: Developer..."
|
||||
)
|
||||
```
|
||||
|
||||
### Example 2: User Preferences (core/user/prefer.md)
|
||||
|
||||
```bash
|
||||
memory_write(
|
||||
path="core/user/prefer.md",
|
||||
description="User habits and code style preferences",
|
||||
tags=["user", "preferences"],
|
||||
content="# User Preferences\n\n## Communication Style\n- Be concise\n- Show code examples\n\n## Code Style\n- 2 space indentation\n- Prefer const over var\n- Functional programming"
|
||||
)
|
||||
```
|
||||
|
||||
### Example 3: Project Architecture (core/project/)
|
||||
|
||||
```bash
|
||||
memory_write(
|
||||
path="core/project/architecture.md",
|
||||
description="Project architecture and design",
|
||||
tags=["project", "architecture"],
|
||||
content="# Architecture\n\n..."
|
||||
)
|
||||
```
|
||||
|
||||
### Example 3: Reference Docs (root level)
|
||||
|
||||
```bash
|
||||
memory_write(
|
||||
path="docs/api/rest-endpoints.md",
|
||||
description="REST API reference documentation",
|
||||
tags=["docs", "api"],
|
||||
content="# REST Endpoints\n\n..."
|
||||
)
|
||||
```
|
||||
|
||||
### Example 4: Archived Decision (root level)
|
||||
|
||||
```bash
|
||||
memory_write(
|
||||
path="archive/decisions/2024-01-15-auth-redesign.md",
|
||||
description="Auth redesign decision from January 2024",
|
||||
tags=["archive", "decision"],
|
||||
content="# Auth Redesign\n\n..."
|
||||
)
|
||||
```
|
||||
|
||||
## Reading Memory Files
|
||||
|
||||
Use the `memory_read` tool:
|
||||
|
||||
```bash
|
||||
memory_read(path="core/user/identity.md")
|
||||
```
|
||||
|
||||
## Listing Memory Files
|
||||
|
||||
Use the `memory_list` tool:
|
||||
|
||||
```bash
|
||||
# List all files
|
||||
memory_list()
|
||||
|
||||
# List files in specific directory
|
||||
memory_list(directory="core/project")
|
||||
|
||||
# List only core/ files
|
||||
memory_list(directory="system")
|
||||
```
|
||||
|
||||
## Updating Memory Files
|
||||
|
||||
To update a file, use `memory_write` with the same path:
|
||||
|
||||
```bash
|
||||
memory_write(
|
||||
path="core/user/identity.md",
|
||||
description="Updated user identity",
|
||||
content="New content..."
|
||||
)
|
||||
```
|
||||
|
||||
The extension preserves existing `created` date and updates `updated` automatically.
|
||||
|
||||
## Folder Creation Guidelines
|
||||
|
||||
### core/ directory - FIXED structure
|
||||
|
||||
**Only two folders exist under `core/`:**
|
||||
|
||||
- `user/` - User identity and preferences
|
||||
- `project/` - Project-specific information
|
||||
|
||||
**Do NOT create any other folders under `core/`.**
|
||||
|
||||
### Root level (same level as core/) - COMPLETE freedom
|
||||
|
||||
**Agent can create any folder structure at project root level (same level as `core/`):**
|
||||
|
||||
- `docs/` - Reference documentation
|
||||
- `archive/` - Historical information
|
||||
- `research/` - Research findings
|
||||
- `notes/` - Standalone notes
|
||||
- `examples/` - Code examples
|
||||
- `guides/` - How-to guides
|
||||
|
||||
**Rule:** Organize root level in a way that makes sense for the project.
|
||||
|
||||
**WARNING:** Do NOT create a `project/` folder at root level. Use `core/project/` instead.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO:
|
||||
|
||||
- Use `core/user/identity.md` for user identity
|
||||
- Use `core/user/prefer.md` for user habits and code style
|
||||
- Use `core/project/` for project-specific information
|
||||
- Use root level for reference, historical, and research content
|
||||
- Keep files focused on a single topic
|
||||
- Organize root level folders by content type
|
||||
|
||||
### DON'T:
|
||||
|
||||
- Create folders under `core/` other than `user/` and `project/`
|
||||
- Create other files under `core/user/` (only `identity.md` and `prefer.md`)
|
||||
- Create a `project/` folder at root level (use `core/project/` instead)
|
||||
- Put reference docs in `core/` (use root `docs/`)
|
||||
- Create giant files (split into focused topics)
|
||||
- Mix unrelated content in same file
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Session Wrap-up
|
||||
|
||||
After completing work, archive to root level:
|
||||
|
||||
```bash
|
||||
memory_write(
|
||||
path="archive/sessions/2025-02-14-bug-fix.md",
|
||||
description="Session summary: fixed database connection bug",
|
||||
tags=["archive", "session"],
|
||||
content="..."
|
||||
)
|
||||
```
|
||||
|
||||
### Regular Cleanup
|
||||
|
||||
- Consolidate duplicate information
|
||||
- Update descriptions to stay accurate
|
||||
- Remove information that's no longer relevant
|
||||
- Archive old content to appropriate root level folders
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use `memory-management` when:
|
||||
|
||||
- User asks to remember something for future sessions
|
||||
- Creating or updating project documentation
|
||||
- Setting preferences or guidelines
|
||||
- Storing reference material
|
||||
- Building knowledge base about the project
|
||||
- Organizing information by type or domain
|
||||
- Creating reusable patterns and solutions
|
||||
- Documenting troubleshooting steps
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `memory-sync` - Git synchronization operations
|
||||
- `memory-init` - Initial repository setup
|
||||
- `memory-search` - Finding specific information
|
||||
- `memory-check` - Validate folder structure before syncing
|
||||
|
||||
## Before Syncing
|
||||
|
||||
**IMPORTANT**: Before running `memory_sync(action="push")`, ALWAYS run `memory_check()` first to verify the folder structure is correct:
|
||||
|
||||
```bash
|
||||
# Check structure first
|
||||
memory_check()
|
||||
|
||||
# Then push if structure is correct
|
||||
memory_sync(action="push")
|
||||
```
|
||||
|
||||
This prevents accidentally pushing files in wrong locations (e.g., root `project/` instead of `core/project/`).
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
---
|
||||
name: memory-search
|
||||
description: Search and retrieve information from pi-memory-md memory files
|
||||
---
|
||||
|
||||
# Memory Search
|
||||
|
||||
Use this skill to find information stored in pi-memory-md memory files.
|
||||
|
||||
## Search Types
|
||||
|
||||
### Search by Content
|
||||
|
||||
Search within markdown content:
|
||||
|
||||
```
|
||||
memory_search(query="typescript", searchIn="content")
|
||||
```
|
||||
|
||||
Returns matching files with content excerpts.
|
||||
|
||||
### Search by Tags
|
||||
|
||||
Find files with specific tags:
|
||||
|
||||
```
|
||||
memory_search(query="user", searchIn="tags")
|
||||
```
|
||||
|
||||
Best for finding files by category or topic.
|
||||
|
||||
### Search by Description
|
||||
|
||||
Find files by their frontmatter description:
|
||||
|
||||
```
|
||||
memory_search(query="identity", searchIn="description")
|
||||
```
|
||||
|
||||
Best for discovering files by purpose.
|
||||
|
||||
## Common Search Patterns
|
||||
|
||||
| Goal | Command |
|
||||
| ---------------- | ------------------------------------------------------------- |
|
||||
| User preferences | `memory_search(query="user", searchIn="tags")` |
|
||||
| Project info | `memory_search(query="architecture", searchIn="description")` |
|
||||
| Code style | `memory_search(query="typescript", searchIn="content")` |
|
||||
| Reference docs | `memory_search(query="reference", searchIn="tags")` |
|
||||
|
||||
## Search Tips
|
||||
|
||||
- **Case insensitive**: `typescript` and `TYPESCRIPT` work the same
|
||||
- **Partial matches**: `auth` matches "auth", "authentication", "author"
|
||||
- **Be specific**: "JWT token validation" > "token"
|
||||
- **Try different types**: If content search fails, try tags or description
|
||||
|
||||
## When Results Are Empty
|
||||
|
||||
1. Check query spelling
|
||||
2. Try different `searchIn` type
|
||||
3. List all files: `memory_list()`
|
||||
4. Sync repository: `memory_sync(action="pull")`
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `memory-management` - Read and write files
|
||||
- `memory-sync` - Ensure latest data
|
||||
- `memory-init` - Setup repository
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
name: memory-sync
|
||||
description: Git synchronization operations for pi-memory-md repository
|
||||
---
|
||||
|
||||
# Memory Sync
|
||||
|
||||
Git synchronization for pi-memory-md repository.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure `pi-memory-md.repoUrl` in settings file (global: `~/.pi/agent/settings.json`, project: `.pi/settings.json`)
|
||||
|
||||
## Sync Operations
|
||||
|
||||
### Pull
|
||||
|
||||
Fetch latest changes from GitHub:
|
||||
|
||||
```
|
||||
memory_sync(action="pull")
|
||||
```
|
||||
|
||||
Use before starting work or switching machines.
|
||||
|
||||
### Push
|
||||
|
||||
Upload local changes to GitHub:
|
||||
|
||||
```
|
||||
memory_sync(action="push")
|
||||
```
|
||||
|
||||
Auto-commits changes before pushing.
|
||||
|
||||
**Before pushing, ALWAYS run memory_check first:**
|
||||
|
||||
```
|
||||
memory_check()
|
||||
```
|
||||
|
||||
This verifies that the folder structure is correct (e.g., files are in `core/project/` not in a root `project/` folder).
|
||||
|
||||
### Status
|
||||
|
||||
Check uncommitted changes:
|
||||
|
||||
```
|
||||
memory_sync(action="status")
|
||||
```
|
||||
|
||||
Shows modified/added/deleted files.
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
| Action | Command |
|
||||
| -------------- | ------------------------------ |
|
||||
| Get updates | `memory_sync(action="pull")` |
|
||||
| Check changes | `memory_sync(action="status")` |
|
||||
| Upload changes | `memory_sync(action="push")` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Error | Solution |
|
||||
| ----------------- | --------------------------------------- |
|
||||
| Non-fast-forward | Pull first, then push |
|
||||
| Conflicts | Manual resolution via bash git commands |
|
||||
| Not a git repo | Run `memory_init(force=true)` |
|
||||
| Permission denied | Check SSH keys or repo URL |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `memory-management` - Read and write files
|
||||
- `memory-init` - Setup repository
|
||||
|
|
@ -1,732 +0,0 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
|
||||
import { keyHint } from "@mariozechner/pi-coding-agent";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { MemoryFrontmatter, MemoryMdSettings } from "./memory-md.js";
|
||||
import {
|
||||
getCurrentDate,
|
||||
getMemoryDir,
|
||||
gitExec,
|
||||
listMemoryFiles,
|
||||
readMemoryFile,
|
||||
syncRepository,
|
||||
writeMemoryFile,
|
||||
} from "./memory-md.js";
|
||||
|
||||
function renderWithExpandHint(
|
||||
text: string,
|
||||
theme: Theme,
|
||||
lineCount: number,
|
||||
): Text {
|
||||
const remaining = lineCount - 1;
|
||||
if (remaining > 0) {
|
||||
text +=
|
||||
"\n" +
|
||||
theme.fg("muted", `... (${remaining} more lines,`) +
|
||||
" " +
|
||||
keyHint("expandTools", "to expand") +
|
||||
theme.fg("muted", ")");
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
}
|
||||
|
||||
export function registerMemorySync(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
isRepoInitialized: { value: boolean },
|
||||
): void {
|
||||
pi.registerTool({
|
||||
name: "memory_sync",
|
||||
label: "Memory Sync",
|
||||
description: "Synchronize memory repository with git (pull/push/status)",
|
||||
parameters: Type.Object({
|
||||
action: Type.Union(
|
||||
[Type.Literal("pull"), Type.Literal("push"), Type.Literal("status")],
|
||||
{
|
||||
description: "Action to perform",
|
||||
},
|
||||
),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const { action } = params as { action: "pull" | "push" | "status" };
|
||||
const localPath = settings.localPath!;
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const coreUserDir = path.join(memoryDir, "core", "user");
|
||||
|
||||
if (action === "status") {
|
||||
const initialized =
|
||||
isRepoInitialized.value && fs.existsSync(coreUserDir);
|
||||
if (!initialized) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Memory repository not initialized. Use memory_init to set up.",
|
||||
},
|
||||
],
|
||||
details: { initialized: false },
|
||||
};
|
||||
}
|
||||
|
||||
const result = await gitExec(pi, localPath, "status", "--porcelain");
|
||||
const dirty = result.stdout.trim().length > 0;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: dirty
|
||||
? `Changes detected:\n${result.stdout}`
|
||||
: "No uncommitted changes",
|
||||
},
|
||||
],
|
||||
details: { initialized: true, dirty },
|
||||
};
|
||||
}
|
||||
|
||||
if (action === "pull") {
|
||||
const result = await syncRepository(pi, settings, isRepoInitialized);
|
||||
return {
|
||||
content: [{ type: "text", text: result.message }],
|
||||
details: { success: result.success },
|
||||
};
|
||||
}
|
||||
|
||||
if (action === "push") {
|
||||
const statusResult = await gitExec(
|
||||
pi,
|
||||
localPath,
|
||||
"status",
|
||||
"--porcelain",
|
||||
);
|
||||
const hasChanges = statusResult.stdout.trim().length > 0;
|
||||
|
||||
if (hasChanges) {
|
||||
await gitExec(pi, localPath, "add", ".");
|
||||
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:.]/g, "-")
|
||||
.slice(0, 19);
|
||||
const commitMessage = `Update memory - ${timestamp}`;
|
||||
const commitResult = await gitExec(
|
||||
pi,
|
||||
localPath,
|
||||
"commit",
|
||||
"-m",
|
||||
commitMessage,
|
||||
);
|
||||
|
||||
if (!commitResult.success) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: "Commit failed - nothing pushed" },
|
||||
],
|
||||
details: { success: false },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = await gitExec(pi, localPath, "push");
|
||||
if (result.success) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: hasChanges
|
||||
? `Committed and pushed changes to repository`
|
||||
: `No changes to commit, repository up to date`,
|
||||
},
|
||||
],
|
||||
details: { success: true, committed: hasChanges },
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: "Push failed - check git status" }],
|
||||
details: { success: false },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "Unknown action" }],
|
||||
details: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("memory_sync "));
|
||||
text += theme.fg("accent", args.action);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
const content = result.content[0];
|
||||
if (content?.type !== "text") {
|
||||
return new Text(theme.fg("dim", "Empty result"), 0, 0);
|
||||
}
|
||||
|
||||
if (isPartial) {
|
||||
return new Text(theme.fg("warning", "Syncing..."), 0, 0);
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
const lines = content.text.split("\n");
|
||||
const summary = lines[0];
|
||||
return renderWithExpandHint(
|
||||
theme.fg("success", summary),
|
||||
theme,
|
||||
lines.length,
|
||||
);
|
||||
}
|
||||
|
||||
return new Text(theme.fg("toolOutput", content.text), 0, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMemoryRead(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
): void {
|
||||
pi.registerTool({
|
||||
name: "memory_read",
|
||||
label: "Memory Read",
|
||||
description: "Read a memory file by path",
|
||||
parameters: Type.Object({
|
||||
path: Type.String({
|
||||
description:
|
||||
"Relative path to memory file (e.g., 'core/user/identity.md')",
|
||||
}),
|
||||
}) as any,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const { path: relPath } = params as { path: string };
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const fullPath = path.join(memoryDir, relPath);
|
||||
|
||||
const memory = readMemoryFile(fullPath);
|
||||
if (!memory) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Failed to read memory file: ${relPath}` },
|
||||
],
|
||||
details: { error: true },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `# ${memory.frontmatter.description}\n\nTags: ${memory.frontmatter.tags?.join(", ") || "none"}\n\n${memory.content}`,
|
||||
},
|
||||
],
|
||||
details: { frontmatter: memory.frontmatter },
|
||||
};
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("memory_read "));
|
||||
text += theme.fg("accent", args.path);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
const details = result.details as
|
||||
| { error?: boolean; frontmatter?: MemoryFrontmatter }
|
||||
| undefined;
|
||||
const content = result.content[0];
|
||||
|
||||
if (isPartial) {
|
||||
return new Text(theme.fg("warning", "Reading..."), 0, 0);
|
||||
}
|
||||
|
||||
if (details?.error) {
|
||||
const text = content?.type === "text" ? content.text : "Error";
|
||||
return new Text(theme.fg("error", text), 0, 0);
|
||||
}
|
||||
|
||||
const desc = details?.frontmatter?.description || "Memory file";
|
||||
const tags = details?.frontmatter?.tags?.join(", ") || "none";
|
||||
const text = content?.type === "text" ? content.text : "";
|
||||
|
||||
if (!expanded) {
|
||||
const lines = text.split("\n");
|
||||
const summary = `${theme.fg("success", desc)}\n${theme.fg("muted", `Tags: ${tags}`)}`;
|
||||
return renderWithExpandHint(summary, theme, lines.length + 2);
|
||||
}
|
||||
|
||||
let resultText = theme.fg("success", desc);
|
||||
resultText += `\n${theme.fg("muted", `Tags: ${tags}`)}`;
|
||||
if (text) {
|
||||
resultText += `\n${theme.fg("toolOutput", text)}`;
|
||||
}
|
||||
return new Text(resultText, 0, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMemoryWrite(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
): void {
|
||||
pi.registerTool({
|
||||
name: "memory_write",
|
||||
label: "Memory Write",
|
||||
description: "Create or update a memory file with YAML frontmatter",
|
||||
parameters: Type.Object({
|
||||
path: Type.String({
|
||||
description:
|
||||
"Relative path to memory file (e.g., 'core/user/identity.md')",
|
||||
}),
|
||||
content: Type.String({ description: "Markdown content" }),
|
||||
description: Type.String({ description: "Description for frontmatter" }),
|
||||
tags: Type.Optional(Type.Array(Type.String())),
|
||||
}) as any,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const {
|
||||
path: relPath,
|
||||
content,
|
||||
description,
|
||||
tags,
|
||||
} = params as {
|
||||
path: string;
|
||||
content: string;
|
||||
description: string;
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const fullPath = path.join(memoryDir, relPath);
|
||||
|
||||
const existing = readMemoryFile(fullPath);
|
||||
const existingFrontmatter = existing?.frontmatter || { description };
|
||||
|
||||
const frontmatter: MemoryFrontmatter = {
|
||||
...existingFrontmatter,
|
||||
description,
|
||||
updated: getCurrentDate(),
|
||||
...(tags && { tags }),
|
||||
};
|
||||
|
||||
writeMemoryFile(fullPath, content, frontmatter);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: `Memory file written: ${relPath}` }],
|
||||
details: { path: fullPath, frontmatter },
|
||||
};
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("memory_write "));
|
||||
text += theme.fg("accent", args.path);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
const content = result.content[0];
|
||||
if (content?.type !== "text") {
|
||||
return new Text(theme.fg("dim", "Empty result"), 0, 0);
|
||||
}
|
||||
|
||||
if (isPartial) {
|
||||
return new Text(theme.fg("warning", "Writing..."), 0, 0);
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
const details = result.details as
|
||||
| { frontmatter?: MemoryFrontmatter }
|
||||
| undefined;
|
||||
const lineCount = details?.frontmatter ? 3 : 1;
|
||||
return renderWithExpandHint(
|
||||
theme.fg("success", `Written: ${content.text}`),
|
||||
theme,
|
||||
lineCount,
|
||||
);
|
||||
}
|
||||
|
||||
const details = result.details as
|
||||
| { path?: string; frontmatter?: MemoryFrontmatter }
|
||||
| undefined;
|
||||
let text = theme.fg("success", content.text);
|
||||
if (details?.frontmatter) {
|
||||
const fm = details.frontmatter;
|
||||
text += `\n${theme.fg("muted", `Description: ${fm.description}`)}`;
|
||||
if (fm.tags) {
|
||||
text += `\n${theme.fg("muted", `Tags: ${fm.tags.join(", ")}`)}`;
|
||||
}
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMemoryList(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
): void {
|
||||
pi.registerTool({
|
||||
name: "memory_list",
|
||||
label: "Memory List",
|
||||
description: "List all memory files in the repository",
|
||||
parameters: Type.Object({
|
||||
directory: Type.Optional(
|
||||
Type.String({ description: "Filter by directory (e.g., 'core/user')" }),
|
||||
),
|
||||
}) as any,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const { directory } = params as { directory?: string };
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const searchDir = directory ? path.join(memoryDir, directory) : memoryDir;
|
||||
const files = listMemoryFiles(searchDir);
|
||||
const relPaths = files.map((f) => path.relative(memoryDir, f));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory files (${relPaths.length}):\n\n${relPaths.map((p) => ` - ${p}`).join("\n")}`,
|
||||
},
|
||||
],
|
||||
details: { files: relPaths, count: relPaths.length },
|
||||
};
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("memory_list"));
|
||||
if (args.directory) {
|
||||
text += ` ${theme.fg("accent", args.directory)}`;
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
const details = result.details as { count?: number } | undefined;
|
||||
|
||||
if (isPartial) {
|
||||
return new Text(theme.fg("warning", "Listing..."), 0, 0);
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
const count = details?.count ?? 0;
|
||||
const content = result.content[0];
|
||||
const lines = content?.type === "text" ? content.text.split("\n") : [];
|
||||
return renderWithExpandHint(
|
||||
theme.fg("success", `${count} memory files`),
|
||||
theme,
|
||||
lines.length,
|
||||
);
|
||||
}
|
||||
|
||||
const content = result.content[0];
|
||||
const text = content?.type === "text" ? content.text : "";
|
||||
return new Text(theme.fg("toolOutput", text), 0, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMemorySearch(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
): void {
|
||||
pi.registerTool({
|
||||
name: "memory_search",
|
||||
label: "Memory Search",
|
||||
description: "Search memory files by content or tags",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "Search query" }),
|
||||
searchIn: Type.Union(
|
||||
[
|
||||
Type.Literal("content"),
|
||||
Type.Literal("tags"),
|
||||
Type.Literal("description"),
|
||||
],
|
||||
{
|
||||
description: "Where to search",
|
||||
},
|
||||
),
|
||||
}) as any,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const { query, searchIn } = params as {
|
||||
query: string;
|
||||
searchIn: "content" | "tags" | "description";
|
||||
};
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
const files = listMemoryFiles(memoryDir);
|
||||
const results: Array<{ path: string; match: string }> = [];
|
||||
|
||||
const queryLower = query.toLowerCase();
|
||||
|
||||
for (const filePath of files) {
|
||||
const memory = readMemoryFile(filePath);
|
||||
if (!memory) continue;
|
||||
|
||||
const relPath = path.relative(memoryDir, filePath);
|
||||
const { frontmatter, content } = memory;
|
||||
|
||||
if (searchIn === "content") {
|
||||
if (content.toLowerCase().includes(queryLower)) {
|
||||
const lines = content.split("\n");
|
||||
const matchLine = lines.find((line) =>
|
||||
line.toLowerCase().includes(queryLower),
|
||||
);
|
||||
results.push({
|
||||
path: relPath,
|
||||
match: matchLine || content.substring(0, 100),
|
||||
});
|
||||
}
|
||||
} else if (searchIn === "tags") {
|
||||
if (
|
||||
frontmatter.tags?.some((tag) =>
|
||||
tag.toLowerCase().includes(queryLower),
|
||||
)
|
||||
) {
|
||||
results.push({
|
||||
path: relPath,
|
||||
match: `Tags: ${frontmatter.tags?.join(", ")}`,
|
||||
});
|
||||
}
|
||||
} else if (searchIn === "description") {
|
||||
if (frontmatter.description.toLowerCase().includes(queryLower)) {
|
||||
results.push({ path: relPath, match: frontmatter.description });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${results.length} result(s):\n\n${results.map((r) => ` ${r.path}\n ${r.match}`).join("\n\n")}`,
|
||||
},
|
||||
],
|
||||
details: { results, count: results.length },
|
||||
};
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("memory_search "));
|
||||
text += theme.fg("accent", `"${args.query}"`);
|
||||
text += ` ${theme.fg("muted", args.searchIn)}`;
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
const details = result.details as { count?: number } | undefined;
|
||||
|
||||
if (isPartial) {
|
||||
return new Text(theme.fg("warning", "Searching..."), 0, 0);
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
const count = details?.count ?? 0;
|
||||
const content = result.content[0];
|
||||
const lines = content?.type === "text" ? content.text.split("\n") : [];
|
||||
return renderWithExpandHint(
|
||||
theme.fg("success", `${count} result(s)`),
|
||||
theme,
|
||||
lines.length,
|
||||
);
|
||||
}
|
||||
|
||||
const content = result.content[0];
|
||||
const text = content?.type === "text" ? content.text : "";
|
||||
return new Text(theme.fg("toolOutput", text), 0, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMemoryInit(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
isRepoInitialized: { value: boolean },
|
||||
): void {
|
||||
pi.registerTool({
|
||||
name: "memory_init",
|
||||
label: "Memory Init",
|
||||
description:
|
||||
"Initialize memory repository (clone or create initial structure)",
|
||||
parameters: Type.Object({
|
||||
force: Type.Optional(
|
||||
Type.Boolean({ description: "Reinitialize even if already set up" }),
|
||||
),
|
||||
}) as any,
|
||||
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
||||
const { force = false } = params as { force?: boolean };
|
||||
|
||||
if (isRepoInitialized.value && !force) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Memory repository already initialized. Use force: true to reinitialize.",
|
||||
},
|
||||
],
|
||||
details: { initialized: true },
|
||||
};
|
||||
}
|
||||
|
||||
const result = await syncRepository(pi, settings, isRepoInitialized);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: result.success
|
||||
? `Memory repository initialized:\n${result.message}\n\nCreated directory structure:\n${["core/user", "core/project", "reference"].map((d) => ` - ${d}`).join("\n")}`
|
||||
: `Initialization failed: ${result.message}`,
|
||||
},
|
||||
],
|
||||
details: { success: result.success },
|
||||
};
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("memory_init"));
|
||||
if (args.force) {
|
||||
text += ` ${theme.fg("warning", "--force")}`;
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
const details = result.details as
|
||||
| { initialized?: boolean; success?: boolean }
|
||||
| undefined;
|
||||
const content = result.content[0];
|
||||
|
||||
if (isPartial) {
|
||||
return new Text(theme.fg("warning", "Initializing..."), 0, 0);
|
||||
}
|
||||
|
||||
if (details?.initialized) {
|
||||
return new Text(theme.fg("muted", "Already initialized"), 0, 0);
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
const success = details?.success;
|
||||
const contentText = content?.type === "text" ? content.text : "";
|
||||
const lines = contentText.split("\n");
|
||||
const summary = success
|
||||
? theme.fg("success", "Initialized")
|
||||
: theme.fg("error", "Initialization failed");
|
||||
return renderWithExpandHint(summary, theme, lines.length);
|
||||
}
|
||||
|
||||
const text = content?.type === "text" ? content.text : "";
|
||||
return new Text(theme.fg("toolOutput", text), 0, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMemoryCheck(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
): void {
|
||||
pi.registerTool({
|
||||
name: "memory_check",
|
||||
label: "Memory Check",
|
||||
description: "Check current project memory folder structure",
|
||||
parameters: Type.Object({}) as any,
|
||||
|
||||
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
||||
const memoryDir = getMemoryDir(settings, ctx);
|
||||
|
||||
if (!fs.existsSync(memoryDir)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory directory not found: ${memoryDir}\n\nProject memory may not be initialized yet.`,
|
||||
},
|
||||
],
|
||||
details: { exists: false },
|
||||
};
|
||||
}
|
||||
|
||||
const { execSync } = await import("node:child_process");
|
||||
let treeOutput = "";
|
||||
|
||||
try {
|
||||
treeOutput = execSync(`tree -L 3 -I "node_modules" "${memoryDir}"`, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
} catch {
|
||||
try {
|
||||
treeOutput = execSync(
|
||||
`find "${memoryDir}" -type d -not -path "*/node_modules/*" | head -20`,
|
||||
{
|
||||
encoding: "utf-8",
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
treeOutput =
|
||||
"Unable to generate directory tree. Please check permissions.";
|
||||
}
|
||||
}
|
||||
|
||||
const files = listMemoryFiles(memoryDir);
|
||||
const relPaths = files.map((f) => path.relative(memoryDir, f));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Memory directory structure for project: ${path.basename(ctx.cwd)}\n\nPath: ${memoryDir}\n\n${treeOutput}\n\nMemory files (${relPaths.length}):\n${relPaths.map((p) => ` ${p}`).join("\n")}`,
|
||||
},
|
||||
],
|
||||
details: { path: memoryDir, fileCount: relPaths.length },
|
||||
};
|
||||
},
|
||||
|
||||
renderCall(_args, theme) {
|
||||
return new Text(theme.fg("toolTitle", theme.bold("memory_check")), 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
const details = result.details as
|
||||
| { exists?: boolean; path?: string; fileCount?: number }
|
||||
| undefined;
|
||||
const content = result.content[0];
|
||||
|
||||
if (isPartial) {
|
||||
return new Text(theme.fg("warning", "Checking..."), 0, 0);
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
const exists = details?.exists ?? true;
|
||||
const fileCount = details?.fileCount ?? 0;
|
||||
const contentText = content?.type === "text" ? content.text : "";
|
||||
const lines = contentText.split("\n");
|
||||
const summary = exists
|
||||
? theme.fg("success", `Structure: ${fileCount} files`)
|
||||
: theme.fg("error", "Not initialized");
|
||||
return renderWithExpandHint(summary, theme, lines.length);
|
||||
}
|
||||
|
||||
const text = content?.type === "text" ? content.text : "";
|
||||
return new Text(theme.fg("toolOutput", text), 0, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerAllTools(
|
||||
pi: ExtensionAPI,
|
||||
settings: MemoryMdSettings,
|
||||
isRepoInitialized: { value: boolean },
|
||||
): void {
|
||||
registerMemorySync(pi, settings, isRepoInitialized);
|
||||
registerMemoryRead(pi, settings);
|
||||
registerMemoryWrite(pi, settings);
|
||||
registerMemoryList(pi, settings);
|
||||
registerMemorySearch(pi, settings);
|
||||
registerMemoryInit(pi, settings, isRepoInitialized);
|
||||
registerMemoryCheck(pi, settings);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue