1
0

update lexicon

This commit is contained in:
2026-03-01 15:40:09 +09:00
parent 3365e7634d
commit 28eeb2be4d
8 changed files with 314 additions and 59 deletions

View File

@@ -19,3 +19,4 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
anyhow = "1.0" anyhow = "1.0"
dirs = "5.0" dirs = "5.0"
chrono = "0.4.44"

View File

@@ -2,32 +2,72 @@
## Overview ## Overview
MCP server for AI memory. Reads/writes core.md and memory.md. Nothing more. MCP server for AI memory. Reads/writes core.json and memory/*.json in atproto lexicon record format.
## Design ## Design
- AI decides, tool records - AI decides, tool records
- File I/O only, no database - File I/O only, no database
- 4 MCP tools: read_core, read_memory, save_memory, compress - 4 MCP tools: read_core, read_memory, save_memory, compress
- Simple, unbreakable, long-lasting - Storage format: atproto getRecord JSON
## MCP Tools ## MCP Tools
| Tool | Args | Description | | Tool | Args | Description |
|------|------|-------------| |------|------|-------------|
| read_core | none | Returns core.md content | | read_core | none | Returns core.json record |
| read_memory | none | Returns memory.md content | | read_memory | none | Returns latest memory record |
| save_memory | content: string | Overwrites memory.md | | save_memory | content: string | Creates new memory record (version increments) |
| compress | conversation: string | Reads memory.md + conversation, writes compressed result to memory.md | | compress | conversation: string | Same as save_memory (AI compresses before calling) |
compress note: AI decides what to keep/discard. Tool just writes. compress note: AI decides what to keep/discard. Tool just writes.
## Data ## Data
``` ```
~/.config/aigpt/ ~/Library/Application Support/ai.syui.gpt/ (macOS)
├── core.md ← read only (identity, settings) ~/.local/share/ai.syui.gpt/ (Linux)
── memory.md ← read/write (memories, grows over time) ── core.json ← read only, rkey: self
└── memory/
├── {tid1}.json ← version 1
├── {tid2}.json ← version 2
└── {tid3}.json ← version 3 (latest)
```
## Record Format
core (single record, rkey: self):
```json
{
"uri": "at://{did}/ai.syui.gpt.core/self",
"value": {
"$type": "ai.syui.gpt.core",
"did": "did:plc:xxx",
"handle": "ai.syui.ai",
"content": {
"$type": "ai.syui.gpt.core#markdown",
"text": "personality and instructions"
},
"createdAt": "2025-01-01T00:00:00Z"
}
}
```
memory (multiple records, rkey: tid):
```json
{
"uri": "at://{did}/ai.syui.gpt.memory/{tid}",
"value": {
"$type": "ai.syui.gpt.memory",
"did": "did:plc:xxx",
"content": {
"$type": "ai.syui.gpt.memory#markdown",
"text": "# Memory\n\n## ..."
},
"version": 5,
"createdAt": "2026-03-01T12:00:00Z"
}
}
``` ```
## Architecture ## Architecture
@@ -35,8 +75,8 @@ compress note: AI decides what to keep/discard. Tool just writes.
``` ```
src/ src/
├── mcp/server.rs ← JSON-RPC over stdio ├── mcp/server.rs ← JSON-RPC over stdio
├── core/reader.rs ← read core.md, memory.md ├── core/reader.rs ← read core.json, memory/*.json
├── core/writer.rs ← write memory.md ├── core/writer.rs ← write memory/{tid}.json
└── main.rs ← CLI + MCP server └── main.rs ← CLI + MCP server
``` ```
@@ -46,20 +86,20 @@ When compress is called, AI should:
- Keep facts and decisions - Keep facts and decisions
- Discard procedures and processes - Discard procedures and processes
- Resolve contradictions (keep newer) - Resolve contradictions (keep newer)
- Don't duplicate core.md content - Don't duplicate core.json content
## Usage ## Usage
```bash ```bash
aigpt serve # start MCP server aigpt server # start MCP server
aigpt read-core # CLI: read core.md aigpt read-core # CLI: read core.json
aigpt read-memory # CLI: read memory.md aigpt read-memory # CLI: read latest memory
aigpt save-memory "content" # CLI: write memory.md aigpt save-memory "..." # CLI: create new memory record
``` ```
## Tech ## Tech
- Rust, MCP (JSON-RPC over stdio), file I/O only - Rust, MCP (JSON-RPC over stdio), atproto record format, file I/O only
## History ## History

117
src/core/config.rs Normal file
View File

@@ -0,0 +1,117 @@
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::fs;
use std::path::PathBuf;
use chrono::Utc;
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub path: Option<String>,
pub did: Option<String>,
pub handle: Option<String>,
}
pub fn config_file() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("ai.syui.gpt")
.join("config.json")
}
pub fn load() -> Config {
let path = config_file();
match fs::read_to_string(&path) {
Ok(content) => serde_json::from_str(&content).unwrap_or(defaults()),
Err(_) => defaults(),
}
}
fn defaults() -> Config {
Config {
path: None,
did: None,
handle: None,
}
}
pub fn init() {
let cfg_path = config_file();
if !cfg_path.exists() {
if let Some(parent) = cfg_path.parent() {
let _ = fs::create_dir_all(parent);
}
let default_cfg = json!({
"path": null,
"did": null,
"handle": null
});
let _ = fs::write(&cfg_path, serde_json::to_string_pretty(&default_cfg).unwrap());
}
let cfg = load();
let core_path = record_path(&cfg, "ai.syui.gpt.core", "self");
if !core_path.exists() {
if let Some(parent) = core_path.parent() {
let _ = fs::create_dir_all(parent);
}
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string());
let handle = cfg.handle.clone().unwrap_or_else(|| "self".to_string());
let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let core_record = json!({
"uri": format!("at://{}/ai.syui.gpt.core/self", did),
"value": {
"$type": "ai.syui.gpt.core",
"did": did,
"handle": handle,
"content": {
"$type": "ai.syui.gpt.core#markdown",
"text": ""
},
"createdAt": now
}
});
let _ = fs::write(&core_path, serde_json::to_string_pretty(&core_record).unwrap());
}
let memory_dir = collection_dir(&cfg, "ai.syui.gpt.memory");
let _ = fs::create_dir_all(&memory_dir);
}
pub fn base_dir(cfg: &Config) -> PathBuf {
match &cfg.path {
Some(p) => {
if p.starts_with("~/") {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(&p[2..])
} else {
PathBuf::from(p)
}
}
None => dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("ai.syui.gpt"),
}
}
pub fn identity(cfg: &Config) -> String {
if cfg!(windows) {
cfg.handle.clone().unwrap_or_else(|| "self".to_string())
} else {
cfg.did.clone().unwrap_or_else(|| "self".to_string())
}
}
/// $cfg/{did|handle}/{collection}/{rkey}.json
pub fn record_path(cfg: &Config, collection: &str, rkey: &str) -> PathBuf {
base_dir(cfg)
.join(identity(cfg))
.join(collection)
.join(format!("{}.json", rkey))
}
/// $cfg/{did|handle}/{collection}/
pub fn collection_dir(cfg: &Config, collection: &str) -> PathBuf {
base_dir(cfg).join(identity(cfg)).join(collection)
}

View File

@@ -1,2 +1,3 @@
pub mod config;
pub mod reader; pub mod reader;
pub mod writer; pub mod writer;

View File

@@ -1,24 +1,37 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use serde_json::Value;
use std::fs; use std::fs;
use std::path::PathBuf;
fn config_dir() -> PathBuf { use crate::core::config;
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from(".")) pub fn read_core() -> Result<Value> {
.join("ai.syui.gpt") let cfg = config::load();
let path = config::record_path(&cfg, "ai.syui.gpt.core", "self");
let content = fs::read_to_string(&path)
.with_context(|| format!("Failed to read {}", path.display()))?;
let record: Value = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse {}", path.display()))?;
Ok(record)
} }
pub fn read_core() -> Result<String> { pub fn read_memory() -> Result<Option<Value>> {
let path = config_dir().join("core.md"); let cfg = config::load();
fs::read_to_string(&path) let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory");
.with_context(|| format!("Failed to read {}", path.display())) if !dir.exists() {
} return Ok(None);
pub fn read_memory() -> Result<String> {
let path = config_dir().join("memory.md");
match fs::read_to_string(&path) {
Ok(content) => Ok(content),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
Err(e) => Err(e).with_context(|| format!("Failed to read {}", path.display())),
} }
let mut files: Vec<_> = fs::read_dir(&dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
.collect();
if files.is_empty() {
return Ok(None);
}
files.sort_by_key(|e| e.file_name());
let latest = files.last().unwrap().path();
let content = fs::read_to_string(&latest)
.with_context(|| format!("Failed to read {}", latest.display()))?;
let record: Value = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse {}", latest.display()))?;
Ok(Some(record))
} }

View File

@@ -1,18 +1,59 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use chrono::Utc;
use serde_json::json;
use std::fs; use std::fs;
use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH};
fn config_dir() -> PathBuf { use crate::core::{config, reader};
dirs::config_dir()
.unwrap_or_else(|| PathBuf::from(".")) fn generate_tid() -> String {
.join("ai.syui.gpt") const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
let micros = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
let mut tid = [0u8; 13];
let mut v = micros;
for i in (0..13).rev() {
tid[i] = CHARSET[(v & 0x1f) as usize];
v >>= 5;
}
String::from_utf8(tid.to_vec()).unwrap()
}
fn next_version() -> u64 {
match reader::read_memory() {
Ok(Some(record)) => record["value"]["version"].as_u64().unwrap_or(0) + 1,
_ => 1,
}
} }
pub fn save_memory(content: &str) -> Result<()> { pub fn save_memory(content: &str) -> Result<()> {
let dir = config_dir(); let cfg = config::load();
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string());
let tid = generate_tid();
let version = next_version();
let now = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let record = json!({
"uri": format!("at://{}/ai.syui.gpt.memory/{}", did, tid),
"value": {
"$type": "ai.syui.gpt.memory",
"did": did,
"content": {
"$type": "ai.syui.gpt.memory#markdown",
"text": content
},
"version": version,
"createdAt": now
}
});
let dir = config::collection_dir(&cfg, "ai.syui.gpt.memory");
fs::create_dir_all(&dir) fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create {}", dir.display()))?; .with_context(|| format!("Failed to create {}", dir.display()))?;
let path = dir.join("memory.md"); let path = dir.join(format!("{}.json", tid));
fs::write(&path, content) let json_str = serde_json::to_string_pretty(&record)?;
fs::write(&path, json_str)
.with_context(|| format!("Failed to write {}", path.display())) .with_context(|| format!("Failed to write {}", path.display()))
} }

View File

@@ -1,16 +1,16 @@
use anyhow::Result; use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use aigpt::core::{reader, writer}; use aigpt::core::{config, reader, writer};
use aigpt::mcp::MCPServer; use aigpt::mcp::MCPServer;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "aigpt")] #[command(name = "aigpt")]
#[command(about = "AI memory MCP server - read/write core.md and memory.md")] #[command(about = "AI memory MCP server")]
#[command(version)] #[command(version)]
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Option<Commands>,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -18,13 +18,13 @@ enum Commands {
/// Start MCP server (JSON-RPC over stdio) /// Start MCP server (JSON-RPC over stdio)
Server, Server,
/// Read core.md /// Read core.json
ReadCore, ReadCore,
/// Read memory.md /// Read latest memory record
ReadMemory, ReadMemory,
/// Save content to memory.md /// Create new memory record
SaveMemory { SaveMemory {
/// Content to write /// Content to write
content: String, content: String,
@@ -32,29 +32,70 @@ enum Commands {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
config::init();
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { match cli.command {
Commands::Server => { None => {
print_status();
}
Some(Commands::Server) => {
let server = MCPServer::new(); let server = MCPServer::new();
server.run()?; server.run()?;
} }
Commands::ReadCore => { Some(Commands::ReadCore) => {
let content = reader::read_core()?; let record = reader::read_core()?;
print!("{}", content); println!("{}", serde_json::to_string_pretty(&record)?);
} }
Commands::ReadMemory => { Some(Commands::ReadMemory) => {
let content = reader::read_memory()?; match reader::read_memory()? {
print!("{}", content); Some(record) => println!("{}", serde_json::to_string_pretty(&record)?),
None => println!("No memory records found"),
}
} }
Commands::SaveMemory { content } => { Some(Commands::SaveMemory { content }) => {
writer::save_memory(&content)?; writer::save_memory(&content)?;
println!("Saved to memory.md"); println!("Saved.");
} }
} }
Ok(()) Ok(())
} }
fn print_status() {
let cfg = config::load();
let did = cfg.did.clone().unwrap_or_else(|| "self".to_string());
let handle = cfg.handle.clone().unwrap_or_else(|| "self".to_string());
let base = config::base_dir(&cfg);
let id = config::identity(&cfg);
let memory_dir = config::collection_dir(&cfg, "ai.syui.gpt.memory");
let memory_count = std::fs::read_dir(&memory_dir)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "json"))
.count()
})
.unwrap_or(0);
let latest_version = match reader::read_memory() {
Ok(Some(record)) => record["value"]["version"].as_u64().unwrap_or(0),
_ => 0,
};
println!("aigpt - AI memory MCP server\n");
println!("config: {}", config::config_file().display());
println!("did: {}", did);
println!("handle: {}", handle);
println!();
println!("path: {}/", base.display());
println!(" {}/{}", id, "ai.syui.gpt.core/self.json");
println!(" {}/{}", id, "ai.syui.gpt.memory/*.json");
println!();
println!("memory: {} records (version: {})", memory_count, latest_version);
}

View File

@@ -162,14 +162,15 @@ impl MCPServer {
fn tool_read_core(&self) -> Value { fn tool_read_core(&self) -> Value {
match reader::read_core() { match reader::read_core() {
Ok(content) => json!({ "content": content }), Ok(record) => record,
Err(e) => json!({ "error": e.to_string() }), Err(e) => json!({ "error": e.to_string() }),
} }
} }
fn tool_read_memory(&self) -> Value { fn tool_read_memory(&self) -> Value {
match reader::read_memory() { match reader::read_memory() {
Ok(content) => json!({ "content": content }), Ok(Some(record)) => record,
Ok(None) => json!({ "content": "" }),
Err(e) => json!({ "error": e.to_string() }), Err(e) => json!({ "error": e.to_string() }),
} }
} }