agent-office 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +44 -0
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +18 -1
- package/dist/commands/worker.d.ts +4 -0
- package/dist/commands/worker.js +63 -0
- package/dist/manage/app.js +1 -1
- package/dist/manage/components/SessionList.js +266 -0
- package/dist/manage/hooks/useApi.d.ts +24 -0
- package/dist/manage/hooks/useApi.js +24 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +3 -3
- package/dist/server/memory.d.ts +64 -0
- package/dist/server/memory.js +214 -0
- package/dist/server/routes.d.ts +3 -2
- package/dist/server/routes.js +274 -2
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ program
|
|
|
13
13
|
.option("--opencode-url <url>", "OpenCode server URL", process.env.OPENCODE_URL ?? "http://localhost:4096")
|
|
14
14
|
.option("--host <host>", "Host to bind to", "127.0.0.1")
|
|
15
15
|
.option("--port <port>", "Port to serve on", "7654")
|
|
16
|
+
.option("--memory-path <path>", "Directory for memory storage (default: ./.memory)", "./.memory")
|
|
16
17
|
.option("--password <password>", "REQUIRED. API password", process.env.AGENT_OFFICE_PASSWORD)
|
|
17
18
|
.action(async (options) => {
|
|
18
19
|
const { serve } = await import("./commands/serve.js");
|
|
@@ -157,4 +158,47 @@ cronCmd
|
|
|
157
158
|
const { cronHistory } = await import("./commands/worker.js");
|
|
158
159
|
await cronHistory(token, cronId);
|
|
159
160
|
});
|
|
161
|
+
// ── Worker Memory Commands (nested) ──────────────────────────────────────────
|
|
162
|
+
const memoryCmd = workerCmd
|
|
163
|
+
.command("memory")
|
|
164
|
+
.description("Manage your persistent memories");
|
|
165
|
+
memoryCmd
|
|
166
|
+
.command("add")
|
|
167
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
168
|
+
.description("Add a new memory")
|
|
169
|
+
.requiredOption("--content <content>", "Memory content to store")
|
|
170
|
+
.action(async (token, options) => {
|
|
171
|
+
const { memoryAdd } = await import("./commands/worker.js");
|
|
172
|
+
await memoryAdd(token, options.content);
|
|
173
|
+
});
|
|
174
|
+
memoryCmd
|
|
175
|
+
.command("search")
|
|
176
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
177
|
+
.description("Search memories using hybrid search (keyword + semantic)")
|
|
178
|
+
.requiredOption("--query <query>", "Search query")
|
|
179
|
+
.option("--limit <limit>", "Maximum results (default 10)", "10")
|
|
180
|
+
.action(async (token, options) => {
|
|
181
|
+
const limit = parseInt(options.limit ?? "10", 10);
|
|
182
|
+
const { memorySearch } = await import("./commands/worker.js");
|
|
183
|
+
await memorySearch(token, options.query, limit);
|
|
184
|
+
});
|
|
185
|
+
memoryCmd
|
|
186
|
+
.command("list")
|
|
187
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
188
|
+
.description("List all stored memories")
|
|
189
|
+
.option("--limit <limit>", "Maximum memories to list (default 50)", "50")
|
|
190
|
+
.action(async (token, options) => {
|
|
191
|
+
const limit = parseInt(options.limit ?? "50", 10);
|
|
192
|
+
const { memoryList } = await import("./commands/worker.js");
|
|
193
|
+
await memoryList(token, limit);
|
|
194
|
+
});
|
|
195
|
+
memoryCmd
|
|
196
|
+
.command("forget")
|
|
197
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
198
|
+
.argument("<memoryId>", "ID of the memory to forget")
|
|
199
|
+
.description("Delete a memory by ID")
|
|
200
|
+
.action(async (token, memoryId) => {
|
|
201
|
+
const { memoryForget } = await import("./commands/worker.js");
|
|
202
|
+
await memoryForget(token, memoryId);
|
|
203
|
+
});
|
|
160
204
|
program.parse();
|
package/dist/commands/serve.d.ts
CHANGED
package/dist/commands/serve.js
CHANGED
|
@@ -3,6 +3,7 @@ import { runMigrations } from "../db/migrate.js";
|
|
|
3
3
|
import { createOpencodeClient } from "../lib/opencode.js";
|
|
4
4
|
import { createApp } from "../server/index.js";
|
|
5
5
|
import { CronScheduler } from "../server/cron.js";
|
|
6
|
+
import { MemoryManager } from "../server/memory.js";
|
|
6
7
|
export async function serve(options) {
|
|
7
8
|
const password = options.password;
|
|
8
9
|
if (!password) {
|
|
@@ -36,10 +37,25 @@ export async function serve(options) {
|
|
|
36
37
|
// Init OpenCode client
|
|
37
38
|
const opencode = createOpencodeClient(options.opencodeUrl);
|
|
38
39
|
const serverUrl = `http://${options.host}:${port}`;
|
|
40
|
+
// Create memory manager and verify embedding model
|
|
41
|
+
const memoryManager = new MemoryManager(options.memoryPath);
|
|
42
|
+
console.log("Warming up embedding model...");
|
|
43
|
+
try {
|
|
44
|
+
await memoryManager.warmup();
|
|
45
|
+
console.log("Embedding model ready.");
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error("Embedding model failed to load:", err);
|
|
49
|
+
console.error("Try deleting the model cache and restarting:");
|
|
50
|
+
console.error(` rm -rf ${options.memoryPath}/.model-cache`);
|
|
51
|
+
memoryManager.closeAll();
|
|
52
|
+
await sql.end();
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
39
55
|
// Create cron scheduler
|
|
40
56
|
const cronScheduler = new CronScheduler();
|
|
41
57
|
// Create Express app
|
|
42
|
-
const app = createApp(sql, opencode, password, serverUrl, cronScheduler);
|
|
58
|
+
const app = createApp(sql, opencode, password, serverUrl, cronScheduler, memoryManager);
|
|
43
59
|
// Start cron scheduler
|
|
44
60
|
await cronScheduler.start(sql, opencode);
|
|
45
61
|
// Start server
|
|
@@ -51,6 +67,7 @@ export async function serve(options) {
|
|
|
51
67
|
console.log("\nShutting down...");
|
|
52
68
|
server.close(async () => {
|
|
53
69
|
cronScheduler.stop();
|
|
70
|
+
memoryManager.closeAll();
|
|
54
71
|
await sql.end();
|
|
55
72
|
console.log("Goodbye.");
|
|
56
73
|
process.exit(0);
|
|
@@ -13,3 +13,7 @@ export declare function deleteCron(token: string, cronId: number): Promise<void>
|
|
|
13
13
|
export declare function enableCron(token: string, cronId: number): Promise<void>;
|
|
14
14
|
export declare function disableCron(token: string, cronId: number): Promise<void>;
|
|
15
15
|
export declare function cronHistory(token: string, cronId: number): Promise<void>;
|
|
16
|
+
export declare function memoryAdd(token: string, content: string): Promise<void>;
|
|
17
|
+
export declare function memorySearch(token: string, query: string, limit: number): Promise<void>;
|
|
18
|
+
export declare function memoryList(token: string, limit: number): Promise<void>;
|
|
19
|
+
export declare function memoryForget(token: string, memoryId: string): Promise<void>;
|
package/dist/commands/worker.js
CHANGED
|
@@ -167,3 +167,66 @@ export async function cronHistory(token, cronId) {
|
|
|
167
167
|
const history = await fetchWorker(token, `/worker/crons/${cronId}/history`);
|
|
168
168
|
console.log(JSON.stringify(history, null, 2));
|
|
169
169
|
}
|
|
170
|
+
// ── Memory Commands ──────────────────────────────────────────────────────────
|
|
171
|
+
export async function memoryAdd(token, content) {
|
|
172
|
+
const result = await postWorker(token, "/worker/memory/add", { content });
|
|
173
|
+
console.log(JSON.stringify(result, null, 2));
|
|
174
|
+
}
|
|
175
|
+
export async function memorySearch(token, query, limit) {
|
|
176
|
+
const result = await postWorker(token, "/worker/memory/search", { query, limit });
|
|
177
|
+
console.log(JSON.stringify(result, null, 2));
|
|
178
|
+
}
|
|
179
|
+
export async function memoryList(token, limit) {
|
|
180
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
181
|
+
const url = `${serverUrl}/worker/memory/list?code=${encodeURIComponent(agentCode)}&limit=${limit}`;
|
|
182
|
+
let res;
|
|
183
|
+
try {
|
|
184
|
+
res = await fetch(url);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
console.error(`Error: could not reach ${serverUrl}`);
|
|
188
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
let body;
|
|
192
|
+
try {
|
|
193
|
+
body = await res.json();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
console.error(`Error: invalid response from server`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
const msg = body.error ?? `HTTP ${res.status}`;
|
|
201
|
+
console.error(`Error: ${msg}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
console.log(JSON.stringify(body, null, 2));
|
|
205
|
+
}
|
|
206
|
+
export async function memoryForget(token, memoryId) {
|
|
207
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
208
|
+
const url = `${serverUrl}/worker/memory/${encodeURIComponent(memoryId)}?code=${encodeURIComponent(agentCode)}`;
|
|
209
|
+
let res;
|
|
210
|
+
try {
|
|
211
|
+
res = await fetch(url, { method: "DELETE" });
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
console.error(`Error: could not reach ${serverUrl}`);
|
|
215
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
let body;
|
|
219
|
+
try {
|
|
220
|
+
body = await res.json();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
console.error(`Error: invalid response from server`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
const msg = body.error ?? `HTTP ${res.status}`;
|
|
228
|
+
console.error(`Error: ${msg}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
console.log(JSON.stringify(body, null, 2));
|
|
232
|
+
}
|
package/dist/manage/app.js
CHANGED
|
@@ -23,7 +23,7 @@ const FOOTER_HINTS = {
|
|
|
23
23
|
connecting: "",
|
|
24
24
|
"auth-error": "",
|
|
25
25
|
menu: "↑↓ navigate · Enter select · q quit",
|
|
26
|
-
list: "↑↓ navigate · c create · d delete · r reveal code · g regen · x revert · t tail · i inject · m mail · Esc back",
|
|
26
|
+
list: "↑↓ navigate · c create · d delete · r reveal code · g regen · x revert · t tail · i inject · m mail · M memories · Esc back",
|
|
27
27
|
"send-message": "Enter submit · Esc back to menu",
|
|
28
28
|
"my-mail": "↑↓ select message · r reply · m mark read · a mark all read · s sent tab · Esc back",
|
|
29
29
|
"profile": "Enter submit · Esc back to menu",
|
|
@@ -211,6 +211,267 @@ function CoworkerMailView({ serverUrl, password, sessionName, contentHeight, onC
|
|
|
211
211
|
? _jsx(Text, { bold: true, color: "yellow", children: "[Sent]" })
|
|
212
212
|
: _jsx(Text, { dimColor: true, children: "Sent (s)" })] }), renderMessages()] }));
|
|
213
213
|
}
|
|
214
|
+
function MemoryView({ serverUrl, password, sessionName, contentHeight, onClose }) {
|
|
215
|
+
const { listMemories, searchMemories, deleteMemory, updateMemory, addMemory } = useApi(serverUrl, password);
|
|
216
|
+
const [memoryMode, setMemoryMode] = useState("browse");
|
|
217
|
+
const [memories, setMemories] = useState([]);
|
|
218
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
219
|
+
const [total, setTotal] = useState(0);
|
|
220
|
+
const [loading, setLoading] = useState(true);
|
|
221
|
+
const [error, setError] = useState(null);
|
|
222
|
+
const [cursor, setCursor] = useState(0);
|
|
223
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
224
|
+
const [selectedMemory, setSelectedMemory] = useState(null);
|
|
225
|
+
const [actionMsg, setActionMsg] = useState(null);
|
|
226
|
+
const [searchInput, setSearchInput] = useState(false);
|
|
227
|
+
const loadMemories = useCallback(async () => {
|
|
228
|
+
setLoading(true);
|
|
229
|
+
setError(null);
|
|
230
|
+
try {
|
|
231
|
+
const result = await listMemories(sessionName, 100);
|
|
232
|
+
setMemories(result.memories);
|
|
233
|
+
setTotal(result.total);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
setLoading(false);
|
|
240
|
+
}
|
|
241
|
+
}, [sessionName]);
|
|
242
|
+
useEffect(() => { void loadMemories(); }, [loadMemories]);
|
|
243
|
+
useInput((input, key) => {
|
|
244
|
+
if (memoryMode === "browse") {
|
|
245
|
+
if (key.escape) {
|
|
246
|
+
onClose();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!loading) {
|
|
250
|
+
if (input === "a") {
|
|
251
|
+
setMemoryMode("adding");
|
|
252
|
+
}
|
|
253
|
+
if (input === "s") {
|
|
254
|
+
setSearchInput(true);
|
|
255
|
+
setMemoryMode("search");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!loading && memories.length > 0) {
|
|
259
|
+
if (key.upArrow)
|
|
260
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
261
|
+
if (key.downArrow)
|
|
262
|
+
setCursor((c) => Math.min(memories.length - 1, c + 1));
|
|
263
|
+
if (key.return) {
|
|
264
|
+
setSelectedMemory(memories[cursor] ?? null);
|
|
265
|
+
setMemoryMode("view-detail");
|
|
266
|
+
setScrollOffset(0);
|
|
267
|
+
}
|
|
268
|
+
if (input === "d") {
|
|
269
|
+
setSelectedMemory(memories[cursor] ?? null);
|
|
270
|
+
setMemoryMode("confirm-delete");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (memoryMode === "view-detail") {
|
|
275
|
+
if (key.escape) {
|
|
276
|
+
setMemoryMode("browse");
|
|
277
|
+
setSelectedMemory(null);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (key.upArrow)
|
|
281
|
+
setScrollOffset((o) => Math.max(0, o - 1));
|
|
282
|
+
if (key.downArrow)
|
|
283
|
+
setScrollOffset((o) => o + 1);
|
|
284
|
+
if (input === "e") {
|
|
285
|
+
setMemoryMode("editing");
|
|
286
|
+
}
|
|
287
|
+
if (input === "d") {
|
|
288
|
+
setMemoryMode("confirm-delete");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (memoryMode === "search-results") {
|
|
292
|
+
if (key.escape) {
|
|
293
|
+
setMemoryMode("browse");
|
|
294
|
+
setSearchResults([]);
|
|
295
|
+
setCursor(0);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (searchResults.length > 0) {
|
|
299
|
+
if (key.upArrow)
|
|
300
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
301
|
+
if (key.downArrow)
|
|
302
|
+
setCursor((c) => Math.min(searchResults.length - 1, c + 1));
|
|
303
|
+
if (key.return) {
|
|
304
|
+
setSelectedMemory(searchResults[cursor] ?? null);
|
|
305
|
+
setMemoryMode("view-detail");
|
|
306
|
+
setScrollOffset(0);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (memoryMode === "confirm-delete") {
|
|
311
|
+
if (key.escape) {
|
|
312
|
+
setMemoryMode(selectedMemory ? "view-detail" : "browse");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (input === "y" || input === "Y") {
|
|
316
|
+
void handleDelete();
|
|
317
|
+
}
|
|
318
|
+
if (input === "n" || input === "N") {
|
|
319
|
+
setMemoryMode(selectedMemory ? "view-detail" : "browse");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (memoryMode === "adding" && key.escape) {
|
|
323
|
+
setMemoryMode("browse");
|
|
324
|
+
}
|
|
325
|
+
if (memoryMode === "search" && key.escape) {
|
|
326
|
+
setSearchInput(false);
|
|
327
|
+
setMemoryMode("browse");
|
|
328
|
+
}
|
|
329
|
+
if (memoryMode === "editing" && key.escape) {
|
|
330
|
+
setMemoryMode("view-detail");
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
const handleDelete = async () => {
|
|
334
|
+
if (!selectedMemory)
|
|
335
|
+
return;
|
|
336
|
+
setMemoryMode("deleting");
|
|
337
|
+
try {
|
|
338
|
+
await deleteMemory(sessionName, selectedMemory.id);
|
|
339
|
+
setActionMsg(`Memory deleted.`);
|
|
340
|
+
setSelectedMemory(null);
|
|
341
|
+
setMemoryMode("browse");
|
|
342
|
+
setCursor(0);
|
|
343
|
+
await loadMemories();
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
347
|
+
setMemoryMode("browse");
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const handleAdd = async (content) => {
|
|
351
|
+
const trimmed = content.trim();
|
|
352
|
+
if (!trimmed)
|
|
353
|
+
return;
|
|
354
|
+
setLoading(true);
|
|
355
|
+
try {
|
|
356
|
+
await addMemory(sessionName, trimmed);
|
|
357
|
+
setActionMsg("Memory added.");
|
|
358
|
+
setMemoryMode("browse");
|
|
359
|
+
await loadMemories();
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
363
|
+
setMemoryMode("browse");
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
setLoading(false);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const handleSearch = async (query) => {
|
|
370
|
+
const trimmed = query.trim();
|
|
371
|
+
if (!trimmed)
|
|
372
|
+
return;
|
|
373
|
+
setSearchInput(false);
|
|
374
|
+
setLoading(true);
|
|
375
|
+
try {
|
|
376
|
+
const results = await searchMemories(sessionName, trimmed, 20);
|
|
377
|
+
setSearchResults(results);
|
|
378
|
+
setCursor(0);
|
|
379
|
+
setMemoryMode("search-results");
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
383
|
+
setMemoryMode("browse");
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
setLoading(false);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const handleEdit = async (newContent) => {
|
|
390
|
+
const trimmed = newContent.trim();
|
|
391
|
+
if (!trimmed || !selectedMemory)
|
|
392
|
+
return;
|
|
393
|
+
setLoading(true);
|
|
394
|
+
try {
|
|
395
|
+
await updateMemory(sessionName, selectedMemory.id, trimmed);
|
|
396
|
+
setActionMsg("Memory updated.");
|
|
397
|
+
setSelectedMemory(null);
|
|
398
|
+
setMemoryMode("browse");
|
|
399
|
+
await loadMemories();
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
403
|
+
setMemoryMode("view-detail");
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
setLoading(false);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
// Clear action messages after a delay
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (actionMsg) {
|
|
412
|
+
const t = setTimeout(() => setActionMsg(null), 2000);
|
|
413
|
+
return () => clearTimeout(t);
|
|
414
|
+
}
|
|
415
|
+
}, [actionMsg]);
|
|
416
|
+
if (loading && memories.length === 0 && memoryMode === "browse") {
|
|
417
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: `Loading memories for "${sessionName}"...` }) }));
|
|
418
|
+
}
|
|
419
|
+
if (memoryMode === "adding") {
|
|
420
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Add Memory" }), _jsx(Text, { color: "cyan", children: sessionName })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Content: " }), _jsx(TextInput, { placeholder: "Type memory content...", onSubmit: (v) => void handleAdd(v) })] }), _jsx(Text, { dimColor: true, children: "Enter to save \u00B7 Esc cancel" })] }));
|
|
421
|
+
}
|
|
422
|
+
if (memoryMode === "search") {
|
|
423
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Search Memories" }), _jsx(Text, { color: "cyan", children: sessionName })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Query: " }), searchInput && (_jsx(TextInput, { placeholder: "semantic or keyword search...", onSubmit: (v) => void handleSearch(v) }))] }), _jsx(Text, { dimColor: true, children: "Enter to search \u00B7 Esc cancel" })] }));
|
|
424
|
+
}
|
|
425
|
+
if (memoryMode === "search-results") {
|
|
426
|
+
const viewHeight = contentHeight - 5;
|
|
427
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Search Results" }), _jsx(Text, { color: "cyan", children: sessionName }), _jsxs(Text, { dimColor: true, children: ["(", searchResults.length, " results)"] })] }), loading && _jsx(Spinner, { label: "Searching..." }), searchResults.length === 0 && !loading ? (_jsx(Text, { dimColor: true, children: "No results found." })) : (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: searchResults.map((mem, i) => {
|
|
428
|
+
const sel = i === cursor;
|
|
429
|
+
const preview = mem.content.length > 80 ? mem.content.slice(0, 80) + "..." : mem.content;
|
|
430
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: sel ? "cyan" : undefined, bold: sel, children: sel ? "▶" : " " }), _jsx(Text, { dimColor: true, children: (mem.score?.toFixed(4) ?? "").padEnd(8) }), _jsx(Text, { color: sel ? "cyan" : undefined, children: preview })] }, mem.id));
|
|
431
|
+
}) })), _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 Enter view \u00B7 Esc back to list" })] }));
|
|
432
|
+
}
|
|
433
|
+
if (memoryMode === "view-detail" && selectedMemory) {
|
|
434
|
+
const lines = selectedMemory.content.split("\n");
|
|
435
|
+
const metaStr = Object.keys(selectedMemory.metadata).length > 0
|
|
436
|
+
? JSON.stringify(selectedMemory.metadata, null, 2)
|
|
437
|
+
: "(none)";
|
|
438
|
+
const allLines = [
|
|
439
|
+
`ID: ${selectedMemory.id}`,
|
|
440
|
+
`Created: ${selectedMemory.createdAt}`,
|
|
441
|
+
...(selectedMemory.score != null ? [`Score: ${selectedMemory.score.toFixed(4)}`] : []),
|
|
442
|
+
`Metadata: ${metaStr}`,
|
|
443
|
+
``,
|
|
444
|
+
`Content:`,
|
|
445
|
+
...lines,
|
|
446
|
+
];
|
|
447
|
+
const viewHeight = contentHeight - 5;
|
|
448
|
+
const maxOff = Math.max(0, allLines.length - viewHeight);
|
|
449
|
+
const clamped = Math.min(scrollOffset, maxOff);
|
|
450
|
+
const visible = allLines.slice(clamped, clamped + viewHeight);
|
|
451
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Memory Detail" }), _jsx(Text, { color: "cyan", children: sessionName })] }), _jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.map((line, i) => (_jsx(Text, { children: line }, i))) }), _jsx(Text, { dimColor: true, children: "\u2191\u2193 scroll \u00B7 e edit \u00B7 d delete \u00B7 Esc back" })] }));
|
|
452
|
+
}
|
|
453
|
+
if (memoryMode === "confirm-delete") {
|
|
454
|
+
const target = selectedMemory ?? memories[cursor];
|
|
455
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Delete Memory" }), _jsx(Text, { color: "cyan", children: sessionName })] }), target && (_jsxs(Text, { children: ["Delete memory: ", _jsxs(Text, { color: "yellow", children: ["\"", target.content.slice(0, 60), "...\""] }), "?"] })), _jsx(Text, { children: "y/n" })] }));
|
|
456
|
+
}
|
|
457
|
+
if (memoryMode === "deleting") {
|
|
458
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Deleting memory..." }) }));
|
|
459
|
+
}
|
|
460
|
+
if (memoryMode === "editing" && selectedMemory) {
|
|
461
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Edit Memory" }), _jsx(Text, { color: "cyan", children: sessionName })] }), _jsxs(Text, { dimColor: true, children: ["Current: ", selectedMemory.content.slice(0, 80), "..."] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "New content: " }), _jsx(TextInput, { placeholder: "Enter new content...", defaultValue: selectedMemory.content, onSubmit: (v) => void handleEdit(v) })] }), _jsx(Text, { dimColor: true, children: "Enter to save \u00B7 Esc cancel" })] }));
|
|
462
|
+
}
|
|
463
|
+
// ── Browse mode ───────────────────────────────────────────────────────────
|
|
464
|
+
const viewHeight = contentHeight - 5;
|
|
465
|
+
const visibleStart = Math.max(0, cursor - viewHeight + 1);
|
|
466
|
+
const visibleMemories = memories.slice(visibleStart, visibleStart + viewHeight);
|
|
467
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Memories" }), _jsx(Text, { color: "cyan", children: sessionName }), _jsxs(Text, { dimColor: true, children: ["(", total, " total)"] }), loading && _jsx(Spinner, {})] }), actionMsg && _jsx(Text, { color: "green", children: actionMsg }), error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), memories.length === 0 ? (_jsx(Text, { dimColor: true, children: "No memories stored yet." })) : (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visibleMemories.map((mem, i) => {
|
|
468
|
+
const actualIdx = visibleStart + i;
|
|
469
|
+
const sel = actualIdx === cursor;
|
|
470
|
+
const preview = mem.content.length > 90 ? mem.content.slice(0, 90) + "..." : mem.content;
|
|
471
|
+
const date = new Date(mem.createdAt).toLocaleDateString();
|
|
472
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: sel ? "cyan" : undefined, bold: sel, children: sel ? "▶" : " " }), _jsx(Text, { dimColor: true, children: date.padEnd(12) }), _jsx(Text, { color: sel ? "cyan" : undefined, children: preview })] }, mem.id));
|
|
473
|
+
}) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 navigate \u00B7 Enter view \u00B7 a add \u00B7 d delete \u00B7 s search \u00B7 Esc back" }) })] }));
|
|
474
|
+
}
|
|
214
475
|
// ─── Main component ──────────────────────────────────────────────────────────
|
|
215
476
|
export function SessionList({ serverUrl, password, contentHeight }) {
|
|
216
477
|
const { listSessions, createSession, deleteSession, regenerateCode, getModes, revertToStart } = useApi(serverUrl, password);
|
|
@@ -290,6 +551,8 @@ export function SessionList({ serverUrl, password, contentHeight }) {
|
|
|
290
551
|
setSubView("inject");
|
|
291
552
|
if (input === "m")
|
|
292
553
|
setSubView("coworker-mail");
|
|
554
|
+
if (input === "M")
|
|
555
|
+
setSubView("memories");
|
|
293
556
|
}
|
|
294
557
|
}
|
|
295
558
|
if (mode === "creating-pick-mode") {
|
|
@@ -450,6 +713,9 @@ export function SessionList({ serverUrl, password, contentHeight }) {
|
|
|
450
713
|
if (subView === "coworker-mail") {
|
|
451
714
|
return (_jsx(CoworkerMailView, { serverUrl: serverUrl, password: password, sessionName: activeSession.name, contentHeight: contentHeight, onClose: closeSubView }));
|
|
452
715
|
}
|
|
716
|
+
if (subView === "memories") {
|
|
717
|
+
return (_jsx(MemoryView, { serverUrl: serverUrl, password: password, sessionName: activeSession.name, contentHeight: contentHeight, onClose: closeSubView }));
|
|
718
|
+
}
|
|
453
719
|
}
|
|
454
720
|
// ── Initial load ──────────────────────────────────────────────────────────
|
|
455
721
|
if (loading && rows.length === 0) {
|
|
@@ -55,6 +55,13 @@ export interface CronHistoryEntry {
|
|
|
55
55
|
success: boolean;
|
|
56
56
|
error_message: string | null;
|
|
57
57
|
}
|
|
58
|
+
export interface MemoryRecord {
|
|
59
|
+
id: string;
|
|
60
|
+
content: string;
|
|
61
|
+
metadata: Record<string, unknown>;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
score?: number;
|
|
64
|
+
}
|
|
58
65
|
export declare function useApi(serverUrl: string, password: string): {
|
|
59
66
|
listSessions: () => Promise<Session[]>;
|
|
60
67
|
createSession: (name: string, mode?: string) => Promise<Session>;
|
|
@@ -106,6 +113,23 @@ export declare function useApi(serverUrl: string, password: string): {
|
|
|
106
113
|
enableCron: (id: number) => Promise<CronJob>;
|
|
107
114
|
disableCron: (id: number) => Promise<CronJob>;
|
|
108
115
|
getCronHistory: (id: number, limit?: number) => Promise<CronHistoryEntry[]>;
|
|
116
|
+
addMemory: (sessionName: string, content: string, metadata?: Record<string, unknown>) => Promise<{
|
|
117
|
+
ok: boolean;
|
|
118
|
+
id: string;
|
|
119
|
+
}>;
|
|
120
|
+
listMemories: (sessionName: string, limit?: number) => Promise<{
|
|
121
|
+
memories: MemoryRecord[];
|
|
122
|
+
total: number;
|
|
123
|
+
}>;
|
|
124
|
+
getMemory: (sessionName: string, memoryId: string) => Promise<MemoryRecord>;
|
|
125
|
+
updateMemory: (sessionName: string, memoryId: string, content: string, metadata?: Record<string, unknown>) => Promise<{
|
|
126
|
+
ok: boolean;
|
|
127
|
+
}>;
|
|
128
|
+
deleteMemory: (sessionName: string, memoryId: string) => Promise<{
|
|
129
|
+
deleted: boolean;
|
|
130
|
+
id: string;
|
|
131
|
+
}>;
|
|
132
|
+
searchMemories: (sessionName: string, query: string, limit?: number) => Promise<MemoryRecord[]>;
|
|
109
133
|
};
|
|
110
134
|
export declare function useAsyncState<T>(): {
|
|
111
135
|
run: (fn: () => Promise<T>) => Promise<T | null>;
|
|
@@ -106,6 +106,24 @@ export function useApi(serverUrl, password) {
|
|
|
106
106
|
const getCronHistory = useCallback(async (id, limit = 10) => {
|
|
107
107
|
return apiFetch(`${base}/crons/${id}/history?limit=${limit}`, password);
|
|
108
108
|
}, [base, password]);
|
|
109
|
+
const addMemory = useCallback(async (sessionName, content, metadata) => {
|
|
110
|
+
return apiFetch(`${base}/sessions/${encodeURIComponent(sessionName)}/memories`, password, { method: "POST", body: JSON.stringify({ content, ...(metadata ? { metadata } : {}) }) });
|
|
111
|
+
}, [base, password]);
|
|
112
|
+
const listMemories = useCallback(async (sessionName, limit = 50) => {
|
|
113
|
+
return apiFetch(`${base}/sessions/${encodeURIComponent(sessionName)}/memories?limit=${limit}`, password);
|
|
114
|
+
}, [base, password]);
|
|
115
|
+
const getMemory = useCallback(async (sessionName, memoryId) => {
|
|
116
|
+
return apiFetch(`${base}/sessions/${encodeURIComponent(sessionName)}/memories/${encodeURIComponent(memoryId)}`, password);
|
|
117
|
+
}, [base, password]);
|
|
118
|
+
const updateMemory = useCallback(async (sessionName, memoryId, content, metadata) => {
|
|
119
|
+
return apiFetch(`${base}/sessions/${encodeURIComponent(sessionName)}/memories/${encodeURIComponent(memoryId)}`, password, { method: "PUT", body: JSON.stringify({ content, ...(metadata ? { metadata } : {}) }) });
|
|
120
|
+
}, [base, password]);
|
|
121
|
+
const deleteMemory = useCallback(async (sessionName, memoryId) => {
|
|
122
|
+
return apiFetch(`${base}/sessions/${encodeURIComponent(sessionName)}/memories/${encodeURIComponent(memoryId)}`, password, { method: "DELETE" });
|
|
123
|
+
}, [base, password]);
|
|
124
|
+
const searchMemories = useCallback(async (sessionName, query, limit = 10) => {
|
|
125
|
+
return apiFetch(`${base}/sessions/${encodeURIComponent(sessionName)}/memories/search`, password, { method: "POST", body: JSON.stringify({ query, limit }) });
|
|
126
|
+
}, [base, password]);
|
|
109
127
|
return {
|
|
110
128
|
listSessions,
|
|
111
129
|
createSession,
|
|
@@ -127,6 +145,12 @@ export function useApi(serverUrl, password) {
|
|
|
127
145
|
enableCron,
|
|
128
146
|
disableCron,
|
|
129
147
|
getCronHistory,
|
|
148
|
+
addMemory,
|
|
149
|
+
listMemories,
|
|
150
|
+
getMemory,
|
|
151
|
+
updateMemory,
|
|
152
|
+
deleteMemory,
|
|
153
|
+
searchMemories,
|
|
130
154
|
};
|
|
131
155
|
}
|
|
132
156
|
export function useAsyncState() {
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Sql } from "../db/index.js";
|
|
2
2
|
import type { OpencodeClient } from "../lib/opencode.js";
|
|
3
3
|
import { CronScheduler } from "./cron.js";
|
|
4
|
-
|
|
4
|
+
import type { MemoryManager } from "./memory.js";
|
|
5
|
+
export declare function createApp(sql: Sql, opencode: OpencodeClient, password: string, serverUrl: string, cronScheduler: CronScheduler, memoryManager: MemoryManager): import("express-serve-static-core").Express;
|
package/dist/server/index.js
CHANGED
|
@@ -10,13 +10,13 @@ function authMiddleware(password) {
|
|
|
10
10
|
next();
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
export function createApp(sql, opencode, password, serverUrl, cronScheduler) {
|
|
13
|
+
export function createApp(sql, opencode, password, serverUrl, cronScheduler, memoryManager) {
|
|
14
14
|
const app = express();
|
|
15
15
|
app.use(express.json());
|
|
16
16
|
// Worker routes are unauthenticated — mounted before auth middleware
|
|
17
|
-
app.use("/", createWorkerRouter(sql, opencode, serverUrl));
|
|
17
|
+
app.use("/", createWorkerRouter(sql, opencode, serverUrl, memoryManager));
|
|
18
18
|
// Everything else requires Bearer auth
|
|
19
19
|
app.use(authMiddleware(password));
|
|
20
|
-
app.use("/", createRouter(sql, opencode, serverUrl, cronScheduler));
|
|
20
|
+
app.use("/", createRouter(sql, opencode, serverUrl, cronScheduler, memoryManager));
|
|
21
21
|
return app;
|
|
22
22
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createAgentMemory, type MemoryEntry } from "fastmemory";
|
|
2
|
+
export interface MemoryRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
content: string;
|
|
5
|
+
metadata: Record<string, unknown>;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
}
|
|
8
|
+
type MemoryStore = Awaited<ReturnType<typeof createAgentMemory>>;
|
|
9
|
+
/**
|
|
10
|
+
* Manages per-session memory stores backed by fastmemory (SQLite + embeddings).
|
|
11
|
+
* Each session gets its own .db file under <memoryPath>/<sessionName>.db
|
|
12
|
+
*/
|
|
13
|
+
export declare class MemoryManager {
|
|
14
|
+
private basePath;
|
|
15
|
+
private stores;
|
|
16
|
+
constructor(memoryPath: string);
|
|
17
|
+
private dbPathFor;
|
|
18
|
+
getStore(sessionName: string): Promise<MemoryStore>;
|
|
19
|
+
/**
|
|
20
|
+
* Add a memory for a session
|
|
21
|
+
*/
|
|
22
|
+
addMemory(sessionName: string, content: string, metadata?: Record<string, unknown>): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize a query string for FTS5 MATCH syntax.
|
|
25
|
+
* Wraps each whitespace-delimited token in double quotes so that
|
|
26
|
+
* special characters (apostrophes, parens, asterisks, etc.) are
|
|
27
|
+
* treated as literals rather than FTS5 operators.
|
|
28
|
+
*/
|
|
29
|
+
private sanitizeFts5Query;
|
|
30
|
+
/**
|
|
31
|
+
* Search memories using hybrid search (BM25 + vector + RRF)
|
|
32
|
+
*/
|
|
33
|
+
searchMemories(sessionName: string, query: string, limit?: number): Promise<MemoryEntry[]>;
|
|
34
|
+
/**
|
|
35
|
+
* List all memories for a session (direct SQLite access since fastmemory has no list method)
|
|
36
|
+
*/
|
|
37
|
+
listMemories(sessionName: string, limit?: number): MemoryRecord[];
|
|
38
|
+
/**
|
|
39
|
+
* Get a single memory by ID
|
|
40
|
+
*/
|
|
41
|
+
getMemory(sessionName: string, memoryId: string): MemoryRecord | null;
|
|
42
|
+
/**
|
|
43
|
+
* Delete a memory by ID (direct SQLite - fastmemory has no delete method)
|
|
44
|
+
*/
|
|
45
|
+
deleteMemory(sessionName: string, memoryId: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Update a memory's content (direct SQLite, re-embeds via delete+add)
|
|
48
|
+
*/
|
|
49
|
+
updateMemory(sessionName: string, memoryId: string, content: string, metadata?: Record<string, unknown>): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Get stats for a session's memory store
|
|
52
|
+
*/
|
|
53
|
+
getStats(sessionName: string): {
|
|
54
|
+
total: number;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Warm up the embedding model and verify it loads correctly.
|
|
58
|
+
* Uses a temporary DB so no real session data is affected.
|
|
59
|
+
* Throws if the model is corrupt or fails to produce embeddings.
|
|
60
|
+
*/
|
|
61
|
+
warmup(): Promise<void>;
|
|
62
|
+
closeAll(): void;
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createAgentMemory } from "fastmemory";
|
|
2
|
+
import { mkdirSync, existsSync } from "fs";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
/**
|
|
6
|
+
* Manages per-session memory stores backed by fastmemory (SQLite + embeddings).
|
|
7
|
+
* Each session gets its own .db file under <memoryPath>/<sessionName>.db
|
|
8
|
+
*/
|
|
9
|
+
export class MemoryManager {
|
|
10
|
+
basePath;
|
|
11
|
+
stores = new Map();
|
|
12
|
+
constructor(memoryPath) {
|
|
13
|
+
this.basePath = resolve(memoryPath);
|
|
14
|
+
if (!existsSync(this.basePath)) {
|
|
15
|
+
mkdirSync(this.basePath, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
dbPathFor(sessionName) {
|
|
19
|
+
// Sanitize session name for filesystem safety
|
|
20
|
+
const safe = sessionName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
21
|
+
return join(this.basePath, `${safe}.db`);
|
|
22
|
+
}
|
|
23
|
+
async getStore(sessionName) {
|
|
24
|
+
if (this.stores.has(sessionName)) {
|
|
25
|
+
return this.stores.get(sessionName);
|
|
26
|
+
}
|
|
27
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
28
|
+
const cacheDir = join(this.basePath, ".model-cache");
|
|
29
|
+
const store = await createAgentMemory({
|
|
30
|
+
dbPath,
|
|
31
|
+
cacheDir,
|
|
32
|
+
dtype: "q4",
|
|
33
|
+
});
|
|
34
|
+
this.stores.set(sessionName, store);
|
|
35
|
+
return store;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Add a memory for a session
|
|
39
|
+
*/
|
|
40
|
+
async addMemory(sessionName, content, metadata = {}) {
|
|
41
|
+
const store = await this.getStore(sessionName);
|
|
42
|
+
return await store.add(content, metadata);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sanitize a query string for FTS5 MATCH syntax.
|
|
46
|
+
* Wraps each whitespace-delimited token in double quotes so that
|
|
47
|
+
* special characters (apostrophes, parens, asterisks, etc.) are
|
|
48
|
+
* treated as literals rather than FTS5 operators.
|
|
49
|
+
*/
|
|
50
|
+
sanitizeFts5Query(query) {
|
|
51
|
+
return query
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter((t) => t.length > 0)
|
|
54
|
+
.map((t) => `"${t.replace(/"/g, '""')}"`)
|
|
55
|
+
.join(" ");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Search memories using hybrid search (BM25 + vector + RRF)
|
|
59
|
+
*/
|
|
60
|
+
async searchMemories(sessionName, query, limit = 10) {
|
|
61
|
+
const store = await this.getStore(sessionName);
|
|
62
|
+
const safeQuery = this.sanitizeFts5Query(query);
|
|
63
|
+
return await store.searchHybrid(safeQuery, limit);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* List all memories for a session (direct SQLite access since fastmemory has no list method)
|
|
67
|
+
*/
|
|
68
|
+
listMemories(sessionName, limit = 50) {
|
|
69
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
70
|
+
if (!existsSync(dbPath))
|
|
71
|
+
return [];
|
|
72
|
+
const db = new Database(dbPath);
|
|
73
|
+
db.pragma("journal_mode = WAL");
|
|
74
|
+
try {
|
|
75
|
+
const rows = db.prepare(`SELECT id, content, metadata, created_at FROM memories ORDER BY created_at DESC LIMIT ?`).all(limit);
|
|
76
|
+
return rows.map((r) => ({
|
|
77
|
+
id: r.id,
|
|
78
|
+
content: r.content,
|
|
79
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : {},
|
|
80
|
+
createdAt: r.created_at,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
db.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get a single memory by ID
|
|
89
|
+
*/
|
|
90
|
+
getMemory(sessionName, memoryId) {
|
|
91
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
92
|
+
if (!existsSync(dbPath))
|
|
93
|
+
return null;
|
|
94
|
+
const db = new Database(dbPath);
|
|
95
|
+
db.pragma("journal_mode = WAL");
|
|
96
|
+
try {
|
|
97
|
+
const row = db.prepare(`SELECT id, content, metadata, created_at FROM memories WHERE id = ?`).get(memoryId);
|
|
98
|
+
if (!row)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
id: row.id,
|
|
102
|
+
content: row.content,
|
|
103
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
104
|
+
createdAt: row.created_at,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Delete a memory by ID (direct SQLite - fastmemory has no delete method)
|
|
113
|
+
*/
|
|
114
|
+
deleteMemory(sessionName, memoryId) {
|
|
115
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
116
|
+
if (!existsSync(dbPath))
|
|
117
|
+
return false;
|
|
118
|
+
const db = new Database(dbPath);
|
|
119
|
+
db.pragma("journal_mode = WAL");
|
|
120
|
+
try {
|
|
121
|
+
const result = db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
|
|
122
|
+
return result.changes > 0;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
db.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Update a memory's content (direct SQLite, re-embeds via delete+add)
|
|
130
|
+
*/
|
|
131
|
+
async updateMemory(sessionName, memoryId, content, metadata) {
|
|
132
|
+
const existing = this.getMemory(sessionName, memoryId);
|
|
133
|
+
if (!existing)
|
|
134
|
+
return false;
|
|
135
|
+
// Delete old and re-add with new content (to get new embedding)
|
|
136
|
+
this.deleteMemory(sessionName, memoryId);
|
|
137
|
+
// Close the cached store so it picks up the direct DB changes
|
|
138
|
+
if (this.stores.has(sessionName)) {
|
|
139
|
+
this.stores.get(sessionName).close();
|
|
140
|
+
this.stores.delete(sessionName);
|
|
141
|
+
}
|
|
142
|
+
const store = await this.getStore(sessionName);
|
|
143
|
+
// Re-add with new content but preserve metadata if not provided
|
|
144
|
+
const finalMetadata = metadata ?? existing.metadata;
|
|
145
|
+
await store.add(content, finalMetadata);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get stats for a session's memory store
|
|
150
|
+
*/
|
|
151
|
+
getStats(sessionName) {
|
|
152
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
153
|
+
if (!existsSync(dbPath))
|
|
154
|
+
return { total: 0 };
|
|
155
|
+
const db = new Database(dbPath);
|
|
156
|
+
db.pragma("journal_mode = WAL");
|
|
157
|
+
try {
|
|
158
|
+
const row = db.prepare(`SELECT COUNT(*) as total FROM memories`).get();
|
|
159
|
+
return { total: row.total };
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
db.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Warm up the embedding model and verify it loads correctly.
|
|
167
|
+
* Uses a temporary DB so no real session data is affected.
|
|
168
|
+
* Throws if the model is corrupt or fails to produce embeddings.
|
|
169
|
+
*/
|
|
170
|
+
async warmup() {
|
|
171
|
+
const testDbPath = join(this.basePath, "_warmup_test.db");
|
|
172
|
+
const cacheDir = join(this.basePath, ".model-cache");
|
|
173
|
+
const store = await createAgentMemory({
|
|
174
|
+
dbPath: testDbPath,
|
|
175
|
+
cacheDir,
|
|
176
|
+
dtype: "q4",
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
// Force an embedding by adding and searching
|
|
180
|
+
const id = await store.add("warmup test memory");
|
|
181
|
+
const results = await store.searchHybrid("warmup test", 1);
|
|
182
|
+
if (results.length === 0) {
|
|
183
|
+
throw new Error("Embedding model warmup: search returned no results");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
store.close();
|
|
188
|
+
// Clean up the temp DB files
|
|
189
|
+
try {
|
|
190
|
+
const { unlinkSync } = await import("fs");
|
|
191
|
+
unlinkSync(testDbPath);
|
|
192
|
+
// SQLite WAL/SHM files
|
|
193
|
+
try {
|
|
194
|
+
unlinkSync(testDbPath + "-wal");
|
|
195
|
+
}
|
|
196
|
+
catch { /* may not exist */ }
|
|
197
|
+
try {
|
|
198
|
+
unlinkSync(testDbPath + "-shm");
|
|
199
|
+
}
|
|
200
|
+
catch { /* may not exist */ }
|
|
201
|
+
}
|
|
202
|
+
catch { /* ignore cleanup errors */ }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
closeAll() {
|
|
206
|
+
for (const [, store] of this.stores) {
|
|
207
|
+
try {
|
|
208
|
+
store.close();
|
|
209
|
+
}
|
|
210
|
+
catch { /* ignore */ }
|
|
211
|
+
}
|
|
212
|
+
this.stores.clear();
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import { Router } from "express";
|
|
|
2
2
|
import type { Sql } from "../db/index.js";
|
|
3
3
|
import type { OpencodeClient } from "../lib/opencode.js";
|
|
4
4
|
import { CronScheduler } from "./cron.js";
|
|
5
|
-
|
|
6
|
-
export declare function
|
|
5
|
+
import type { MemoryManager } from "./memory.js";
|
|
6
|
+
export declare function createRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string, scheduler: CronScheduler, memoryManager: MemoryManager): Router;
|
|
7
|
+
export declare function createWorkerRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string, memoryManager: MemoryManager): Router;
|
package/dist/server/routes.js
CHANGED
|
@@ -73,6 +73,24 @@ function generateWelcomeMessage(name, mode, status, humanName, humanDescription,
|
|
|
73
73
|
` agent-office worker cron \\`,
|
|
74
74
|
` ${token}`,
|
|
75
75
|
``,
|
|
76
|
+
` Store a memory (persistent across sessions)`,
|
|
77
|
+
` agent-office worker memory add \\`,
|
|
78
|
+
` --content "your memory here" \\`,
|
|
79
|
+
` ${token}`,
|
|
80
|
+
``,
|
|
81
|
+
` Search your memories`,
|
|
82
|
+
` agent-office worker memory search \\`,
|
|
83
|
+
` --query "your search" \\`,
|
|
84
|
+
` ${token}`,
|
|
85
|
+
``,
|
|
86
|
+
` List all your memories`,
|
|
87
|
+
` agent-office worker memory list \\`,
|
|
88
|
+
` ${token}`,
|
|
89
|
+
``,
|
|
90
|
+
` Forget a memory`,
|
|
91
|
+
` agent-office worker memory forget \\`,
|
|
92
|
+
` ${token} <memory-id>`,
|
|
93
|
+
``,
|
|
76
94
|
`════════════════════════════════════════════════════════`,
|
|
77
95
|
` ⚠ IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
|
|
78
96
|
`════════════════════════════════════════════════════════`,
|
|
@@ -109,7 +127,7 @@ function generateWelcomeMessage(name, mode, status, humanName, humanDescription,
|
|
|
109
127
|
``,
|
|
110
128
|
].join("\n");
|
|
111
129
|
}
|
|
112
|
-
export function createRouter(sql, opencode, serverUrl, scheduler) {
|
|
130
|
+
export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager) {
|
|
113
131
|
const router = Router();
|
|
114
132
|
router.get("/health", (_req, res) => {
|
|
115
133
|
res.json({ ok: true });
|
|
@@ -799,9 +817,135 @@ export function createRouter(sql, opencode, serverUrl, scheduler) {
|
|
|
799
817
|
res.status(500).json({ error: "Internal server error" });
|
|
800
818
|
}
|
|
801
819
|
});
|
|
820
|
+
// ── Memory Endpoints (authenticated, for manage TUI) ───────────────────────
|
|
821
|
+
router.get("/sessions/:name/memories", async (req, res) => {
|
|
822
|
+
const { name } = req.params;
|
|
823
|
+
const limit = Math.min(parseInt(req.query.limit ?? "50", 10), 200);
|
|
824
|
+
const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
|
|
825
|
+
if (rows.length === 0) {
|
|
826
|
+
res.status(404).json({ error: `Session "${name}" not found` });
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
const memories = memoryManager.listMemories(name, limit);
|
|
831
|
+
const stats = memoryManager.getStats(name);
|
|
832
|
+
res.json({ memories, total: stats.total });
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
console.error("GET /sessions/:name/memories error:", err);
|
|
836
|
+
res.status(500).json({ error: "Internal server error" });
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
router.post("/sessions/:name/memories", async (req, res) => {
|
|
840
|
+
const { name } = req.params;
|
|
841
|
+
const { content, metadata } = req.body;
|
|
842
|
+
if (!content || typeof content !== "string" || !content.trim()) {
|
|
843
|
+
res.status(400).json({ error: "content is required" });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
|
|
847
|
+
if (rows.length === 0) {
|
|
848
|
+
res.status(404).json({ error: `Session "${name}" not found` });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
const id = await memoryManager.addMemory(name, content.trim(), metadata ?? {});
|
|
853
|
+
res.status(201).json({ ok: true, id });
|
|
854
|
+
}
|
|
855
|
+
catch (err) {
|
|
856
|
+
console.error("POST /sessions/:name/memories error:", err);
|
|
857
|
+
res.status(500).json({ error: "Internal server error" });
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
router.get("/sessions/:name/memories/:memoryId", async (req, res) => {
|
|
861
|
+
const { name, memoryId } = req.params;
|
|
862
|
+
const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
|
|
863
|
+
if (rows.length === 0) {
|
|
864
|
+
res.status(404).json({ error: `Session "${name}" not found` });
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
const memory = memoryManager.getMemory(name, memoryId);
|
|
869
|
+
if (!memory) {
|
|
870
|
+
res.status(404).json({ error: "Memory not found" });
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
res.json(memory);
|
|
874
|
+
}
|
|
875
|
+
catch (err) {
|
|
876
|
+
console.error("GET /sessions/:name/memories/:memoryId error:", err);
|
|
877
|
+
res.status(500).json({ error: "Internal server error" });
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
router.put("/sessions/:name/memories/:memoryId", async (req, res) => {
|
|
881
|
+
const { name, memoryId } = req.params;
|
|
882
|
+
const { content, metadata } = req.body;
|
|
883
|
+
if (!content || typeof content !== "string" || !content.trim()) {
|
|
884
|
+
res.status(400).json({ error: "content is required" });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
|
|
888
|
+
if (rows.length === 0) {
|
|
889
|
+
res.status(404).json({ error: `Session "${name}" not found` });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const updated = await memoryManager.updateMemory(name, memoryId, content.trim(), metadata);
|
|
894
|
+
if (!updated) {
|
|
895
|
+
res.status(404).json({ error: "Memory not found" });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
res.json({ ok: true });
|
|
899
|
+
}
|
|
900
|
+
catch (err) {
|
|
901
|
+
console.error("PUT /sessions/:name/memories/:memoryId error:", err);
|
|
902
|
+
res.status(500).json({ error: "Internal server error" });
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
router.delete("/sessions/:name/memories/:memoryId", async (req, res) => {
|
|
906
|
+
const { name, memoryId } = req.params;
|
|
907
|
+
const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
|
|
908
|
+
if (rows.length === 0) {
|
|
909
|
+
res.status(404).json({ error: `Session "${name}" not found` });
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
const deleted = memoryManager.deleteMemory(name, memoryId);
|
|
914
|
+
if (!deleted) {
|
|
915
|
+
res.status(404).json({ error: "Memory not found" });
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
res.json({ deleted: true, id: memoryId });
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
console.error("DELETE /sessions/:name/memories/:memoryId error:", err);
|
|
922
|
+
res.status(500).json({ error: "Internal server error" });
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
router.post("/sessions/:name/memories/search", async (req, res) => {
|
|
926
|
+
const { name } = req.params;
|
|
927
|
+
const { query, limit } = req.body;
|
|
928
|
+
if (!query || typeof query !== "string" || !query.trim()) {
|
|
929
|
+
res.status(400).json({ error: "query is required" });
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const rows = await sql `SELECT id FROM sessions WHERE name = ${name}`;
|
|
933
|
+
if (rows.length === 0) {
|
|
934
|
+
res.status(404).json({ error: `Session "${name}" not found` });
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const results = await memoryManager.searchMemories(name, query.trim(), Math.min(limit ?? 10, 50));
|
|
939
|
+
res.json(results);
|
|
940
|
+
}
|
|
941
|
+
catch (err) {
|
|
942
|
+
console.error("POST /sessions/:name/memories/search error:", err);
|
|
943
|
+
res.status(500).json({ error: "Internal server error" });
|
|
944
|
+
}
|
|
945
|
+
});
|
|
802
946
|
return router;
|
|
803
947
|
}
|
|
804
|
-
export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
948
|
+
export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
|
|
805
949
|
const router = Router();
|
|
806
950
|
router.get("/worker/clock-in", async (req, res) => {
|
|
807
951
|
const { code } = req.query;
|
|
@@ -870,6 +1014,24 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
|
870
1014
|
` agent-office worker cron \\`,
|
|
871
1015
|
` ${token}`,
|
|
872
1016
|
``,
|
|
1017
|
+
` Store a memory (persistent across sessions)`,
|
|
1018
|
+
` agent-office worker memory add \\`,
|
|
1019
|
+
` --content "your memory here" \\`,
|
|
1020
|
+
` ${token}`,
|
|
1021
|
+
``,
|
|
1022
|
+
` Search your memories`,
|
|
1023
|
+
` agent-office worker memory search \\`,
|
|
1024
|
+
` --query "your search" \\`,
|
|
1025
|
+
` ${token}`,
|
|
1026
|
+
``,
|
|
1027
|
+
` List all your memories`,
|
|
1028
|
+
` agent-office worker memory list \\`,
|
|
1029
|
+
` ${token}`,
|
|
1030
|
+
``,
|
|
1031
|
+
` Forget a memory`,
|
|
1032
|
+
` agent-office worker memory forget \\`,
|
|
1033
|
+
` ${token} <memory-id>`,
|
|
1034
|
+
``,
|
|
873
1035
|
`════════════════════════════════════════════════════════`,
|
|
874
1036
|
` ⚠ IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
|
|
875
1037
|
`════════════════════════════════════════════════════════`,
|
|
@@ -1370,5 +1532,115 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
|
1370
1532
|
res.status(500).json({ error: "Internal server error" });
|
|
1371
1533
|
}
|
|
1372
1534
|
});
|
|
1535
|
+
// ── Worker Memory Endpoints (authenticated via agent_code in query) ────────
|
|
1536
|
+
router.post("/worker/memory/add", async (req, res) => {
|
|
1537
|
+
const { code } = req.query;
|
|
1538
|
+
const { content, metadata } = req.body;
|
|
1539
|
+
if (!code || typeof code !== "string") {
|
|
1540
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (!content || typeof content !== "string" || !content.trim()) {
|
|
1544
|
+
res.status(400).json({ error: "content is required" });
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const session = await sql `
|
|
1548
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1549
|
+
`;
|
|
1550
|
+
if (session.length === 0) {
|
|
1551
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
const sessionName = session[0].name;
|
|
1555
|
+
try {
|
|
1556
|
+
const id = await memoryManager.addMemory(sessionName, content.trim(), metadata ?? {});
|
|
1557
|
+
res.status(201).json({ ok: true, id });
|
|
1558
|
+
}
|
|
1559
|
+
catch (err) {
|
|
1560
|
+
console.error("POST /worker/memory/add error:", err);
|
|
1561
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
router.post("/worker/memory/search", async (req, res) => {
|
|
1565
|
+
const { code } = req.query;
|
|
1566
|
+
const { query, limit } = req.body;
|
|
1567
|
+
if (!code || typeof code !== "string") {
|
|
1568
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (!query || typeof query !== "string" || !query.trim()) {
|
|
1572
|
+
res.status(400).json({ error: "query is required" });
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const session = await sql `
|
|
1576
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1577
|
+
`;
|
|
1578
|
+
if (session.length === 0) {
|
|
1579
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const sessionName = session[0].name;
|
|
1583
|
+
try {
|
|
1584
|
+
const results = await memoryManager.searchMemories(sessionName, query.trim(), Math.min(limit ?? 10, 50));
|
|
1585
|
+
res.json(results);
|
|
1586
|
+
}
|
|
1587
|
+
catch (err) {
|
|
1588
|
+
console.error("POST /worker/memory/search error:", err);
|
|
1589
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
router.get("/worker/memory/list", async (req, res) => {
|
|
1593
|
+
const { code, limit: limitStr } = req.query;
|
|
1594
|
+
if (!code || typeof code !== "string") {
|
|
1595
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const session = await sql `
|
|
1599
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1600
|
+
`;
|
|
1601
|
+
if (session.length === 0) {
|
|
1602
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const sessionName = session[0].name;
|
|
1606
|
+
const limit = Math.min(parseInt(limitStr ?? "50", 10), 200);
|
|
1607
|
+
try {
|
|
1608
|
+
const memories = memoryManager.listMemories(sessionName, limit);
|
|
1609
|
+
const stats = memoryManager.getStats(sessionName);
|
|
1610
|
+
res.json({ memories, total: stats.total });
|
|
1611
|
+
}
|
|
1612
|
+
catch (err) {
|
|
1613
|
+
console.error("GET /worker/memory/list error:", err);
|
|
1614
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
router.delete("/worker/memory/:memoryId", async (req, res) => {
|
|
1618
|
+
const { code } = req.query;
|
|
1619
|
+
const { memoryId } = req.params;
|
|
1620
|
+
if (!code || typeof code !== "string") {
|
|
1621
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const session = await sql `
|
|
1625
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1626
|
+
`;
|
|
1627
|
+
if (session.length === 0) {
|
|
1628
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const sessionName = session[0].name;
|
|
1632
|
+
try {
|
|
1633
|
+
const deleted = memoryManager.deleteMemory(sessionName, memoryId);
|
|
1634
|
+
if (!deleted) {
|
|
1635
|
+
res.status(404).json({ error: "Memory not found" });
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
res.json({ deleted: true, id: memoryId });
|
|
1639
|
+
}
|
|
1640
|
+
catch (err) {
|
|
1641
|
+
console.error("DELETE /worker/memory/:memoryId error:", err);
|
|
1642
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1373
1645
|
return router;
|
|
1374
1646
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-office",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Manage OpenCode sessions with named aliases",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,11 +38,13 @@
|
|
|
38
38
|
"croner": "^10.0.1",
|
|
39
39
|
"dotenv": "^16.0.0",
|
|
40
40
|
"express": "^4.18.0",
|
|
41
|
+
"fastmemory": "^0.1.5",
|
|
41
42
|
"ink": "^5.0.0",
|
|
42
43
|
"postgres": "^3.4.0",
|
|
43
44
|
"react": "^18.0.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
47
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
46
48
|
"@types/express": "^4.17.0",
|
|
47
49
|
"@types/node": "^20.0.0",
|
|
48
50
|
"@types/react": "^18.0.0",
|