summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-10 23:41:33 +0000
committersoryu <soryu@soryu.co>2026-02-10 23:41:33 +0000
commit48cb5f2f9917fbf70ed9f342e2bbb52c7200f16d (patch)
tree045474029d9cf3075d69a9c381efd792a4b8b67c
parent339c1769379a851c4126021132573bd4b7994cf2 (diff)
downloadsoryu-makima/makima--add-an-optional-memory-system-for-directiv-c8298c6c.tar.gz
soryu-makima/makima--add-an-optional-memory-system-for-directiv-c8298c6c.zip
feat: makima: Add an optional memory system for directives: Add memory panel to frontend DirectiveDetail componentmakima/makima--add-an-optional-memory-system-for-directiv-c8298c6c
-rw-r--r--makima/frontend/package-lock.json15
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx261
-rw-r--r--makima/frontend/src/hooks/useDirectiveMemories.ts119
-rw-r--r--makima/frontend/src/lib/api.ts67
4 files changed, 447 insertions, 15 deletions
diff --git a/makima/frontend/package-lock.json b/makima/frontend/package-lock.json
index 38adfc4..f1d54d6 100644
--- a/makima/frontend/package-lock.json
+++ b/makima/frontend/package-lock.json
@@ -55,7 +55,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -962,7 +961,6 @@
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.11.0.tgz",
"integrity": "sha512-g1ou5Zw3r4mCU0L+EXH4vRtAiyt8qz1JOvL1k+PW4rZ4+71h5nBy/fLgD7cg5BnzQZmjRO1PzCgpF5BIrlKYxQ==",
"dev": true,
- "peer": true,
"dependencies": {
"@babel/core": "^7.27.7",
"@babel/generator": "^7.27.5",
@@ -1894,7 +1892,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"devOptional": true,
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2043,7 +2040,6 @@
"url": "https://github.com/sponsors/ai"
}
],
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2242,7 +2238,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
- "peer": true,
"engines": {
"node": ">=12"
}
@@ -2937,7 +2932,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3009,7 +3003,6 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3018,7 +3011,6 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3036,7 +3028,6 @@
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
- "peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -3068,7 +3059,6 @@
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz",
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
- "peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -3128,8 +3118,7 @@
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
- "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "peer": true
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -3266,7 +3255,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3358,7 +3346,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
index 1340482..e9e739b 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -1,6 +1,7 @@
import { useState } from "react";
-import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api";
+import type { DirectiveWithSteps, DirectiveStatus, MemoryCategory } from "../../lib/api";
import { DirectiveDAG } from "./DirectiveDAG";
+import { useDirectiveMemories } from "../../hooks/useDirectiveMemories";
const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
@@ -10,6 +11,16 @@ const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> =
archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
};
+const CATEGORY_COLORS: Record<MemoryCategory, { text: string; border: string; bg: string; label: string }> = {
+ decision: { text: "text-amber-400", border: "border-amber-800", bg: "bg-amber-900/20", label: "Decision" },
+ context: { text: "text-cyan-400", border: "border-cyan-800", bg: "bg-cyan-900/20", label: "Context" },
+ preference: { text: "text-violet-400", border: "border-violet-800", bg: "bg-violet-900/20", label: "Preference" },
+ learning: { text: "text-emerald-400", border: "border-emerald-800", bg: "bg-emerald-900/20", label: "Learning" },
+ other: { text: "text-[#7788aa]", border: "border-[#2a3a5a]", bg: "bg-[#1a2540]", label: "Other" },
+};
+
+const ALL_CATEGORIES: MemoryCategory[] = ["decision", "context", "preference", "learning", "other"];
+
interface DirectiveDetailProps {
directive: DirectiveWithSteps;
onStart: () => void;
@@ -43,6 +54,29 @@ export function DirectiveDetail({
const totalSteps = directive.steps.length;
const progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
+ // Memory panel state
+ const [memoryOpen, setMemoryOpen] = useState(false);
+ const [addingMemory, setAddingMemory] = useState(false);
+ const [newCategory, setNewCategory] = useState<MemoryCategory>("context");
+ const [newContent, setNewContent] = useState("");
+ const [newSource, setNewSource] = useState("");
+ const [confirmClear, setConfirmClear] = useState(false);
+
+ const {
+ grouped,
+ config: memoryConfig,
+ loading: memoryLoading,
+ error: memoryError,
+ toggleEnabled,
+ add: addMemory,
+ remove: removeMemory,
+ clearAll: clearMemories,
+ refresh: refreshMemories,
+ } = useDirectiveMemories(directive.id);
+
+ const memoryEnabled = memoryConfig?.enabled ?? false;
+ const totalMemories = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
+
const handleGoalSave = () => {
if (goalText.trim() && goalText !== directive.goal) {
onUpdateGoal(goalText.trim());
@@ -50,6 +84,23 @@ export function DirectiveDetail({
setEditingGoal(false);
};
+ const handleAddMemory = async () => {
+ if (!newContent.trim()) return;
+ await addMemory({
+ category: newCategory,
+ content: newContent.trim(),
+ source: newSource.trim() || undefined,
+ });
+ setNewContent("");
+ setNewSource("");
+ setAddingMemory(false);
+ };
+
+ const handleClearAll = async () => {
+ await clearMemories();
+ setConfirmClear(false);
+ };
+
return (
<div className="flex flex-col h-full overflow-y-auto">
{/* Header */}
@@ -215,6 +266,214 @@ export function DirectiveDetail({
)}
</div>
+ {/* Memory Panel */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
+ {/* Memory header — always visible */}
+ <div className="flex items-center justify-between">
+ <button
+ type="button"
+ onClick={() => setMemoryOpen((v) => !v)}
+ className="flex items-center gap-1.5 group"
+ >
+ <span className="text-[10px] font-mono text-[#556677] group-hover:text-[#9bc3ff] transition-colors">
+ {memoryOpen ? "\u25BC" : "\u25B6"}
+ </span>
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ Memory
+ </span>
+ {totalMemories > 0 && (
+ <span className="text-[9px] font-mono text-[#556677] ml-1">
+ ({totalMemories})
+ </span>
+ )}
+ </button>
+ <div className="flex items-center gap-2">
+ {/* Enable/disable toggle */}
+ <button
+ type="button"
+ onClick={() => toggleEnabled(!memoryEnabled)}
+ className={`text-[9px] font-mono border rounded px-1.5 py-0.5 transition-colors ${
+ memoryEnabled
+ ? "text-emerald-400 border-emerald-800 hover:text-emerald-300"
+ : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
+ }`}
+ title={memoryEnabled ? "Disable memory" : "Enable memory"}
+ >
+ {memoryEnabled ? "ON" : "OFF"}
+ </button>
+ </div>
+ </div>
+
+ {/* Collapsible content */}
+ {memoryOpen && (
+ <div className="mt-2">
+ {memoryError && (
+ <div className="text-[10px] font-mono text-red-400 mb-2 px-2 py-1 bg-red-900/10 border border-red-800/30 rounded">
+ {memoryError}
+ </div>
+ )}
+
+ {memoryLoading ? (
+ <div className="text-[10px] font-mono text-[#556677] py-2">Loading...</div>
+ ) : totalMemories === 0 ? (
+ <div className="text-[10px] font-mono text-[#556677] py-2">
+ No memory entries yet.
+ {!memoryEnabled && " Enable memory to start capturing entries."}
+ </div>
+ ) : (
+ /* Grouped entries */
+ <div className="flex flex-col gap-2">
+ {ALL_CATEGORIES.map((cat) => {
+ const entries = grouped[cat];
+ if (entries.length === 0) return null;
+ const style = CATEGORY_COLORS[cat];
+ return (
+ <div key={cat}>
+ <div className="flex items-center gap-1.5 mb-1">
+ <span className={`text-[9px] font-mono ${style.text} uppercase tracking-wider`}>
+ {style.label}
+ </span>
+ <span className="text-[9px] font-mono text-[#556677]">
+ ({entries.length})
+ </span>
+ </div>
+ <div className="flex flex-col gap-1">
+ {entries.map((entry) => (
+ <div
+ key={entry.id}
+ className={`flex items-start gap-2 px-2 py-1.5 rounded border ${style.border} ${style.bg}`}
+ >
+ <div className="flex-1 min-w-0">
+ <p className="text-[10px] font-mono text-[#c0d0e0] whitespace-pre-wrap break-words">
+ {entry.content}
+ </p>
+ {entry.source && (
+ <span className="text-[9px] font-mono text-[#556677] mt-0.5 block">
+ src: {entry.source}
+ </span>
+ )}
+ </div>
+ <button
+ type="button"
+ onClick={() => removeMemory(entry.id)}
+ className="text-[9px] font-mono text-[#556677] hover:text-red-400 shrink-0 mt-0.5"
+ title="Delete entry"
+ >
+ x
+ </button>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ )}
+
+ {/* Action bar: Add + Clear */}
+ <div className="flex items-center gap-2 mt-2 pt-2 border-t border-[rgba(117,170,252,0.1)]">
+ <button
+ type="button"
+ onClick={() => setAddingMemory((v) => !v)}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.2)] rounded px-2 py-0.5"
+ >
+ {addingMemory ? "Cancel" : "+ Add"}
+ </button>
+ {totalMemories > 0 && (
+ <>
+ {confirmClear ? (
+ <div className="flex items-center gap-1.5 ml-auto">
+ <span className="text-[9px] font-mono text-red-400">Clear all?</span>
+ <button
+ type="button"
+ onClick={handleClearAll}
+ className="text-[9px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-1.5 py-0.5"
+ >
+ Yes
+ </button>
+ <button
+ type="button"
+ onClick={() => setConfirmClear(false)}
+ className="text-[9px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-1.5 py-0.5"
+ >
+ No
+ </button>
+ </div>
+ ) : (
+ <button
+ type="button"
+ onClick={() => setConfirmClear(true)}
+ className="text-[10px] font-mono text-[#556677] hover:text-red-400 ml-auto"
+ >
+ Clear all
+ </button>
+ )}
+ </>
+ )}
+ <button
+ type="button"
+ onClick={refreshMemories}
+ className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
+ title="Refresh memories"
+ >
+ [refresh]
+ </button>
+ </div>
+
+ {/* Add form */}
+ {addingMemory && (
+ <div className="mt-2 p-2 bg-[#0a1628] border border-[rgba(117,170,252,0.15)] rounded flex flex-col gap-2">
+ <div className="flex items-center gap-2">
+ <label className="text-[9px] font-mono text-[#7788aa] shrink-0">Category</label>
+ <select
+ value={newCategory}
+ onChange={(e) => setNewCategory(e.target.value as MemoryCategory)}
+ className="bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-1.5 py-0.5 text-[10px] font-mono text-white flex-1"
+ >
+ {ALL_CATEGORIES.map((c) => (
+ <option key={c} value={c}>
+ {CATEGORY_COLORS[c].label}
+ </option>
+ ))}
+ </select>
+ </div>
+ <textarea
+ value={newContent}
+ onChange={(e) => setNewContent(e.target.value)}
+ placeholder="Memory content..."
+ className="w-full bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[10px] font-mono text-white resize-y min-h-[40px] placeholder:text-[#556677]"
+ rows={2}
+ />
+ <input
+ type="text"
+ value={newSource}
+ onChange={(e) => setNewSource(e.target.value)}
+ placeholder="Source (optional)"
+ className="w-full bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[10px] font-mono text-white placeholder:text-[#556677]"
+ />
+ <div className="flex gap-1.5">
+ <button
+ type="button"
+ onClick={handleAddMemory}
+ disabled={!newContent.trim()}
+ className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5 disabled:opacity-40 disabled:cursor-not-allowed"
+ >
+ Save
+ </button>
+ <button
+ type="button"
+ onClick={() => { setAddingMemory(false); setNewContent(""); setNewSource(""); }}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+
{/* DAG */}
<div className="px-4 py-3 flex-1">
<span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2">
diff --git a/makima/frontend/src/hooks/useDirectiveMemories.ts b/makima/frontend/src/hooks/useDirectiveMemories.ts
new file mode 100644
index 0000000..3844c44
--- /dev/null
+++ b/makima/frontend/src/hooks/useDirectiveMemories.ts
@@ -0,0 +1,119 @@
+import { useState, useEffect, useCallback } from "react";
+import {
+ type DirectiveMemoryEntry,
+ type DirectiveMemoryConfig,
+ type MemoryCategory,
+ type CreateDirectiveMemoryRequest,
+ getDirectiveMemoryConfig,
+ setDirectiveMemoryEnabled,
+ listDirectiveMemories,
+ addDirectiveMemory,
+ deleteDirectiveMemory,
+ clearDirectiveMemories,
+} from "../lib/api";
+
+export function useDirectiveMemories(directiveId: string | undefined) {
+ const [memories, setMemories] = useState<DirectiveMemoryEntry[]>([]);
+ const [config, setConfig] = useState<DirectiveMemoryConfig | null>(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ const refreshConfig = useCallback(async () => {
+ if (!directiveId) return;
+ try {
+ const c = await getDirectiveMemoryConfig(directiveId);
+ setConfig(c);
+ } catch (e) {
+ // Config may not exist yet — treat as disabled
+ setConfig({ directiveId, enabled: false, updatedAt: new Date().toISOString() });
+ }
+ }, [directiveId]);
+
+ const refreshMemories = useCallback(async () => {
+ if (!directiveId) return;
+ try {
+ setLoading(true);
+ setError(null);
+ const entries = await listDirectiveMemories(directiveId);
+ setMemories(entries);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to load memories");
+ } finally {
+ setLoading(false);
+ }
+ }, [directiveId]);
+
+ const refresh = useCallback(async () => {
+ await Promise.all([refreshConfig(), refreshMemories()]);
+ }, [refreshConfig, refreshMemories]);
+
+ useEffect(() => {
+ refresh();
+ }, [refresh]);
+
+ const toggleEnabled = useCallback(async (enabled: boolean) => {
+ if (!directiveId) return;
+ try {
+ setError(null);
+ const c = await setDirectiveMemoryEnabled(directiveId, enabled);
+ setConfig(c);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to toggle memory");
+ }
+ }, [directiveId]);
+
+ const add = useCallback(async (req: CreateDirectiveMemoryRequest) => {
+ if (!directiveId) return;
+ try {
+ setError(null);
+ await addDirectiveMemory(directiveId, req);
+ await refreshMemories();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to add memory");
+ }
+ }, [directiveId, refreshMemories]);
+
+ const remove = useCallback(async (memoryId: string) => {
+ if (!directiveId) return;
+ try {
+ setError(null);
+ await deleteDirectiveMemory(directiveId, memoryId);
+ await refreshMemories();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to delete memory");
+ }
+ }, [directiveId, refreshMemories]);
+
+ const clearAll = useCallback(async () => {
+ if (!directiveId) return;
+ try {
+ setError(null);
+ await clearDirectiveMemories(directiveId);
+ setMemories([]);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to clear memories");
+ }
+ }, [directiveId]);
+
+ /** Group entries by category */
+ const grouped = memories.reduce<Record<MemoryCategory, DirectiveMemoryEntry[]>>(
+ (acc, entry) => {
+ acc[entry.category].push(entry);
+ return acc;
+ },
+ { decision: [], context: [], preference: [], learning: [], other: [] },
+ );
+
+ return {
+ memories,
+ grouped,
+ config,
+ loading,
+ error,
+ refresh,
+ toggleEnabled,
+ add,
+ remove,
+ clearAll,
+ };
+}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index b1422df..e6868fc 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3225,4 +3225,71 @@ export async function updateDirectiveGoal(id: string, goal: string): Promise<Dir
return res.json();
}
+// --- Directive Memory ---
+
+export type MemoryCategory = "decision" | "context" | "preference" | "learning" | "other";
+
+export interface DirectiveMemoryEntry {
+ id: string;
+ directiveId: string;
+ category: MemoryCategory;
+ content: string;
+ source: string | null;
+ createdAt: string;
+}
+
+export interface DirectiveMemoryConfig {
+ directiveId: string;
+ enabled: boolean;
+ updatedAt: string;
+}
+
+export interface CreateDirectiveMemoryRequest {
+ category: MemoryCategory;
+ content: string;
+ source?: string;
+}
+
+export async function getDirectiveMemoryConfig(directiveId: string): Promise<DirectiveMemoryConfig> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory/config`);
+ if (!res.ok) throw new Error(`Failed to get memory config: ${res.statusText}`);
+ return res.json();
+}
+
+export async function setDirectiveMemoryEnabled(directiveId: string, enabled: boolean): Promise<DirectiveMemoryConfig> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory/config`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ enabled }),
+ });
+ if (!res.ok) throw new Error(`Failed to update memory config: ${res.statusText}`);
+ return res.json();
+}
+
+export async function listDirectiveMemories(directiveId: string): Promise<DirectiveMemoryEntry[]> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory`);
+ if (!res.ok) throw new Error(`Failed to list memories: ${res.statusText}`);
+ return res.json();
+}
+
+export async function addDirectiveMemory(directiveId: string, req: CreateDirectiveMemoryRequest): Promise<DirectiveMemoryEntry> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) throw new Error(`Failed to add memory: ${res.statusText}`);
+ return res.json();
+}
+
+export async function deleteDirectiveMemory(directiveId: string, memoryId: string): Promise<void> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory/${memoryId}`, { method: "DELETE" });
+ if (!res.ok) throw new Error(`Failed to delete memory: ${res.statusText}`);
+}
+
+export async function clearDirectiveMemories(directiveId: string): Promise<void> {
+ const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/memory`, { method: "DELETE" });
+ if (!res.ok) throw new Error(`Failed to clear memories: ${res.statusText}`);
+}
+