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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@Edison-A-N:registry=https://npm.pkg.github.com
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"name": "opencode-agent-memory",
"version": "0.2.0",
"name": "@Edison-A-N/opencode-agent-memory",
"version": "0.2.0-a1",
"description": "Letta-style editable memory blocks for OpenCode.",
"author": "Josh Thomas <josh@joshthomas.dev>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/joshuadavidthomas/opencode-agent-memory"
"url": "https://github.com/Edison-A-N/opencode-agent-memory"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
},
"main": "src/plugin.ts",
"type": "module",
Expand Down
5 changes: 5 additions & 0 deletions src/journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const ConfigSchema = z.looseObject({
tags: z.array(TagSchema).optional(),
})
.optional(),
memory: z
.looseObject({
disable_global: z.boolean().optional(),
})
.optional(),
});

export type AgentMemoryConfig = z.infer<typeof ConfigSchema>;
Expand Down
36 changes: 34 additions & 2 deletions src/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,33 @@ function stableSortBlocks(blocks: MemoryBlock[]): MemoryBlock[] {
return blocks;
}

export function createMemoryStore(projectDirectory: string): MemoryStore {
export type MemoryStoreOptions = {
disableGlobal?: boolean;
};

export function createMemoryStore(
projectDirectory: string,
storeOpts?: MemoryStoreOptions,
): MemoryStore {
const disableGlobal = storeOpts?.disableGlobal === true;

function assertGlobalAllowed(scope: MemoryScope): void {
if (disableGlobal && scope === "global") {
throw new Error(
"Global memory scope is disabled. Only project-scoped memory blocks are available.",
);
}
}

return {
async ensureSeed() {
await ensureGitignore(projectDirectory);

for (const seed of SEED_BLOCKS) {
if (disableGlobal && seed.scope === "global") {
continue;
}

const dir = scopeDir(projectDirectory, seed.scope);
await fs.mkdir(dir, { recursive: true });

Expand All @@ -198,7 +219,15 @@ export function createMemoryStore(projectDirectory: string): MemoryStore {
},

async listBlocks(scope) {
const scopes: MemoryScope[] = scope === "all" ? ["global", "project"] : [scope];
let scopes: MemoryScope[];
if (scope === "all") {
scopes = disableGlobal ? ["project"] : ["global", "project"];
} else {
if (disableGlobal && scope === "global") {
return [];
}
scopes = [scope];
}
const blocks: MemoryBlock[] = [];

for (const s of scopes) {
Expand All @@ -225,6 +254,7 @@ export function createMemoryStore(projectDirectory: string): MemoryStore {
},

async getBlock(scope, label) {
assertGlobalAllowed(scope);
const safeLabel = validateLabel(label);
const dir = scopeDir(projectDirectory, scope);
const filePath = path.join(dir, `${safeLabel}.md`);
Expand All @@ -237,6 +267,7 @@ export function createMemoryStore(projectDirectory: string): MemoryStore {
},

async setBlock(scope, label, value, opts) {
assertGlobalAllowed(scope);
const safeLabel = validateLabel(label);
const dir = scopeDir(projectDirectory, scope);
await fs.mkdir(dir, { recursive: true });
Expand Down Expand Up @@ -267,6 +298,7 @@ export function createMemoryStore(projectDirectory: string): MemoryStore {
},

async replaceInBlock(scope, label, oldText, newText) {
assertGlobalAllowed(scope);
const block = await this.getBlock(scope, label);
if (block.readOnly) {
throw new Error(`Memory block is read-only: ${scope}:${block.label}`);
Expand Down
12 changes: 7 additions & 5 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import {
import type { JournalContext } from "./tools";

export const MemoryPlugin: Plugin = async ({ directory }) => {
const store = createMemoryStore(directory);
const config = await loadConfig();
const disableGlobal = config.memory?.disable_global === true;

const store = createMemoryStore(directory, { disableGlobal });
await store.ensureSeed();

// Journal: opt-in via ~/.config/opencode/agent-memory.json
const config = await loadConfig();
const journalEnabled = config.journal?.enabled === true;

// Mutable state updated by chat.message hook
Expand Down Expand Up @@ -70,9 +72,9 @@ export const MemoryPlugin: Plugin = async ({ directory }) => {
},

tool: {
memory_list: MemoryList(store),
memory_set: MemorySet(store),
memory_replace: MemoryReplace(store),
memory_list: MemoryList(store, { disableGlobal }),
memory_set: MemorySet(store, { disableGlobal }),
memory_replace: MemoryReplace(store, { disableGlobal }),
...journalTools,
},
};
Expand Down
34 changes: 25 additions & 9 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ import { tool } from "@opencode-ai/plugin";
import type { JournalStore } from "./journal";
import type { MemoryScope, MemoryStore } from "./memory";

export function MemoryList(store: MemoryStore) {
export type MemoryToolOptions = {
disableGlobal?: boolean;
};

export function MemoryList(store: MemoryStore, opts?: MemoryToolOptions) {
const disableGlobal = opts?.disableGlobal === true;
const scopeValues = disableGlobal
? (["all", "project"] as const)
: (["all", "global", "project"] as const);

return tool({
description: "List available memory blocks (labels, descriptions, sizes).",
args: {
scope: tool.schema.enum(["all", "global", "project"]).optional(),
scope: tool.schema.enum(scopeValues).optional(),
},
async execute(args) {
// Default to "all" for list (show everything)
const scope = (args.scope ?? "all") as MemoryScope | "all";
const blocks = await store.listBlocks(scope);
if (blocks.length === 0) {
Expand All @@ -27,18 +35,22 @@ export function MemoryList(store: MemoryStore) {
});
}

export function MemorySet(store: MemoryStore) {
export function MemorySet(store: MemoryStore, opts?: MemoryToolOptions) {
const disableGlobal = opts?.disableGlobal === true;
const scopeValues = disableGlobal
? (["project"] as const)
: (["global", "project"] as const);

return tool({
description: "Create or update a memory block (full overwrite).",
args: {
label: tool.schema.string(),
scope: tool.schema.enum(["global", "project"]).optional(),
scope: tool.schema.enum(scopeValues).optional(),
value: tool.schema.string(),
description: tool.schema.string().optional(),
limit: tool.schema.number().int().positive().optional(),
},
async execute(args) {
// Default to "project" for mutations (safer default)
const scope = (args.scope ?? "project") as MemoryScope;
await store.setBlock(scope, args.label, args.value, {
description: args.description,
Expand All @@ -49,17 +61,21 @@ export function MemorySet(store: MemoryStore) {
});
}

export function MemoryReplace(store: MemoryStore) {
export function MemoryReplace(store: MemoryStore, opts?: MemoryToolOptions) {
const disableGlobal = opts?.disableGlobal === true;
const scopeValues = disableGlobal
? (["project"] as const)
: (["global", "project"] as const);

return tool({
description: "Replace a substring within a memory block.",
args: {
label: tool.schema.string(),
scope: tool.schema.enum(["global", "project"]).optional(),
scope: tool.schema.enum(scopeValues).optional(),
oldText: tool.schema.string(),
newText: tool.schema.string(),
},
async execute(args) {
// Default to "project" for mutations (safer default)
const scope = (args.scope ?? "project") as MemoryScope;
await store.replaceInBlock(scope, args.label, args.oldText, args.newText);
return `Updated memory block ${scope}:${args.label}.`;
Expand Down