aicf-core
Version:
Universal AI Context Format (AICF) - Enterprise-grade AI memory infrastructure with 95.5% compression and zero semantic loss
311 lines • 10.8 kB
JavaScript
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
* Copyright (c) 2025 Dennis van Leeuwen
*
* AICF Reader - Programmatic access to AI Context Format files
*/
import { join } from "node:path";
import { ok, err, toError } from "./types/result.js";
import { SafeFileSystem } from "./utils/file-system.js";
import { validatePath } from "./security/path-validator.js";
import { validateConfig, SECURE_DEFAULTS, } from "./security/config-validator.js";
import { readFileStream } from "./security/file-operations.js";
/**
* AICF Reader - Read and query AICF files
*/
export class AICFReader {
aicfDir;
fs;
config;
indexCache = null;
lastIndexRead = 0;
constructor(aicfDir = ".aicf", fs, _logger, config) {
const pathResult = validatePath(aicfDir);
if (!pathResult.ok) {
throw pathResult.error;
}
this.aicfDir = pathResult.value;
this.fs = new SafeFileSystem(fs);
validateConfig({
maxFileSize: config?.maxFileSize ?? SECURE_DEFAULTS.maxFileSize,
...config,
});
this.config = {
maxFileSize: config?.maxFileSize ?? SECURE_DEFAULTS.maxFileSize,
enableCaching: config?.enableCaching ?? true,
cacheTimeout: config?.cacheTimeout ?? 5 * 60 * 1000,
};
}
/**
* Read and cache the master index
*/
async getIndex() {
try {
const indexPath = join(this.aicfDir, "index.aicf");
const statResult = await this.fs.stat(indexPath);
if (!statResult.ok) {
return err(statResult.error);
}
const stats = statResult.value;
const mtimeMs = stats.mtimeMs ?? 0;
if (this.config.enableCaching &&
this.indexCache &&
mtimeMs <= this.lastIndexRead) {
return ok(this.indexCache);
}
const contentResult = await this.fs.readFile(indexPath);
if (!contentResult.ok) {
return err(contentResult.error);
}
const parseResult = this.parseIndex(contentResult.value);
if (!parseResult.ok) {
return err(parseResult.error);
}
this.indexCache = parseResult.value;
this.lastIndexRead = mtimeMs;
return ok(this.indexCache);
}
catch (error) {
return err(toError(error));
}
}
/**
* Parse index content
*/
parseIndex(content) {
try {
const lines = content.split("\n").filter(Boolean);
const index = {};
let currentSection = null;
for (const line of lines) {
const parts = line.split("|", 2);
if (parts.length < 2)
continue;
const data = parts[1];
if (!data)
continue;
if (data.startsWith("@")) {
currentSection = data.substring(1);
index[currentSection] = {};
}
else if (currentSection && data.includes("=")) {
const [key, value] = data.split("=", 2);
if (key && value && index[currentSection]) {
index[currentSection][key] = value;
}
}
}
return ok(index);
}
catch (error) {
return err(toError(error));
}
}
/**
* Get last N conversations
*/
async getLastConversations(count = 5) {
try {
const conversationsPath = join(this.aicfDir, "conversations.aicf");
const existsResult = await this.fs.exists(conversationsPath);
if (!existsResult.ok) {
return err(existsResult.error);
}
if (!existsResult.value) {
return ok([]);
}
const statResult = await this.fs.stat(conversationsPath);
if (!statResult.ok) {
return err(statResult.error);
}
const stats = statResult.value;
const fileSize = stats.size ?? 0;
if (fileSize > this.config.maxFileSize) {
return this.getLastConversationsStreaming(conversationsPath, count);
}
else {
return this.getLastConversationsMemory(conversationsPath, count);
}
}
catch (error) {
return err(toError(error));
}
}
/**
* Get last conversations from memory (small files)
*/
async getLastConversationsMemory(conversationsPath, count) {
try {
const contentResult = await this.fs.readFile(conversationsPath);
if (!contentResult.ok) {
return err(contentResult.error);
}
const lines = contentResult.value.split("\n").filter(Boolean);
const conversations = [];
let currentConv = null;
for (let i = lines.length - 1; i >= 0 && conversations.length < count; i--) {
const line = lines[i];
if (!line)
continue;
const parts = line.split("|", 2);
if (parts.length < 2)
continue;
const data = parts[1];
if (!data)
continue;
if (data.startsWith("@CONVERSATION:")) {
if (currentConv && this.isValidConversation(currentConv)) {
conversations.push(currentConv);
}
currentConv = {};
}
else if (currentConv) {
this.parseConversationLine(data, currentConv);
}
}
if (currentConv && this.isValidConversation(currentConv)) {
conversations.push(currentConv);
}
return ok(conversations);
}
catch (error) {
return err(toError(error));
}
}
/**
* Get last conversations using streaming (large files)
*/
async getLastConversationsStreaming(conversationsPath, count) {
try {
const conversations = [];
let currentConv = null;
const streamResult = await readFileStream(conversationsPath, (line) => {
const parts = line.split("|", 2);
if (parts.length < 2)
return;
const data = parts[1];
if (!data)
return;
if (data.startsWith("@CONVERSATION:")) {
if (currentConv && this.isValidConversation(currentConv)) {
conversations.unshift(currentConv);
if (conversations.length > count) {
conversations.pop();
}
}
currentConv = {};
}
else if (currentConv) {
this.parseConversationLine(data, currentConv);
}
});
if (!streamResult.ok) {
return err(streamResult.error);
}
if (currentConv && this.isValidConversation(currentConv)) {
conversations.unshift(currentConv);
}
return ok(conversations.slice(0, count));
}
catch (error) {
return err(toError(error));
}
}
/**
* Parse conversation line
*/
parseConversationLine(data, conv) {
if (data.includes("=")) {
const [key, value] = data.split("=", 2);
if (key && value) {
if (key === "id")
conv.id = value;
else if (key === "timestamp")
conv.timestamp = value;
else if (key === "role")
conv.role = value;
else if (key === "content")
conv.content = value;
}
}
}
/**
* Check if conversation is valid
*/
isValidConversation(conv) {
return !!(conv.id && conv.timestamp && conv.role && conv.content);
}
/**
* Get statistics about AICF data
*/
async getStats() {
try {
const indexResult = await this.getIndex();
if (!indexResult.ok) {
return err(indexResult.error);
}
const index = indexResult.value;
const stats = {
project: {
name: index["METADATA"]?.["project_name"] ?? "Unknown",
path: this.aicfDir,
},
counts: {
conversations: parseInt(index["STATS"]?.["conversations"] ?? "0", 10),
memories: parseInt(index["STATS"]?.["memories"] ?? "0", 10),
decisions: parseInt(index["STATS"]?.["decisions"] ?? "0", 10),
insights: parseInt(index["STATS"]?.["insights"] ?? "0", 10),
},
lastUpdate: index["METADATA"]?.["updated_at"] ?? new Date().toISOString(),
state: {
status: index["STATE"]?.["status"] ?? "active",
},
};
return ok(stats);
}
catch (error) {
return err(toError(error));
}
}
/**
* Get current work state
*/
async getCurrentWorkState() {
try {
const workPath = join(this.aicfDir, "work.aicf");
const existsResult = await this.fs.exists(workPath);
if (!existsResult.ok) {
return err(existsResult.error);
}
if (!existsResult.value) {
return ok(null);
}
const contentResult = await this.fs.readFile(workPath);
if (!contentResult.ok) {
return err(contentResult.error);
}
const lines = contentResult.value.split("\n").filter(Boolean);
if (lines.length === 0) {
return ok(null);
}
const lastLine = lines[lines.length - 1];
if (!lastLine) {
return ok(null);
}
const parts = lastLine.split("|");
if (parts.length < 3) {
return ok(null);
}
return ok({
id: parts[1] ?? "",
status: parts[2] ?? "",
description: parts[3] ?? "",
});
}
catch (error) {
return err(toError(error));
}
}
}
//# sourceMappingURL=aicf-reader.js.map