opencode-claw 0.1.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +316 -0
  3. package/dist/channels/router.d.ts +18 -0
  4. package/dist/channels/router.js +188 -0
  5. package/dist/channels/slack.d.ts +4 -0
  6. package/dist/channels/slack.js +87 -0
  7. package/dist/channels/telegram.d.ts +4 -0
  8. package/dist/channels/telegram.js +79 -0
  9. package/dist/channels/types.d.ts +27 -0
  10. package/dist/channels/types.js +1 -0
  11. package/dist/channels/whatsapp.d.ts +4 -0
  12. package/dist/channels/whatsapp.js +136 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +6 -0
  15. package/dist/compat.d.ts +12 -0
  16. package/dist/compat.js +63 -0
  17. package/dist/config/loader.d.ts +2 -0
  18. package/dist/config/loader.js +57 -0
  19. package/dist/config/schema.d.ts +482 -0
  20. package/dist/config/schema.js +108 -0
  21. package/dist/config/types.d.ts +14 -0
  22. package/dist/config/types.js +1 -0
  23. package/dist/cron/scheduler.d.ts +16 -0
  24. package/dist/cron/scheduler.js +113 -0
  25. package/dist/exports.d.ts +9 -0
  26. package/dist/exports.js +4 -0
  27. package/dist/health/server.d.ts +17 -0
  28. package/dist/health/server.js +68 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +127 -0
  31. package/dist/memory/factory.d.ts +3 -0
  32. package/dist/memory/factory.js +14 -0
  33. package/dist/memory/openviking.d.ts +5 -0
  34. package/dist/memory/openviking.js +138 -0
  35. package/dist/memory/plugin-entry.d.ts +3 -0
  36. package/dist/memory/plugin-entry.js +11 -0
  37. package/dist/memory/plugin.d.ts +5 -0
  38. package/dist/memory/plugin.js +63 -0
  39. package/dist/memory/txt.d.ts +2 -0
  40. package/dist/memory/txt.js +137 -0
  41. package/dist/memory/types.d.ts +36 -0
  42. package/dist/memory/types.js +1 -0
  43. package/dist/outbox/drainer.d.ts +8 -0
  44. package/dist/outbox/drainer.js +140 -0
  45. package/dist/outbox/writer.d.ts +15 -0
  46. package/dist/outbox/writer.js +29 -0
  47. package/dist/sessions/manager.d.ts +20 -0
  48. package/dist/sessions/manager.js +68 -0
  49. package/dist/sessions/persistence.d.ts +4 -0
  50. package/dist/sessions/persistence.js +24 -0
  51. package/dist/utils/logger.d.ts +8 -0
  52. package/dist/utils/logger.js +33 -0
  53. package/dist/utils/reconnect.d.ts +16 -0
  54. package/dist/utils/reconnect.js +54 -0
  55. package/dist/utils/shutdown.d.ts +5 -0
  56. package/dist/utils/shutdown.js +27 -0
  57. package/opencode-claw.example.json +71 -0
  58. package/package.json +77 -0
@@ -0,0 +1,113 @@
1
+ import cron from "node-cron";
2
+ function extractText(parts) {
3
+ return parts
4
+ .filter((p) => p.type === "text" && typeof p.text === "string")
5
+ .map((p) => p.text)
6
+ .join("\n\n");
7
+ }
8
+ export function createCronScheduler(deps) {
9
+ const jobs = new Map();
10
+ const running = new Set();
11
+ async function executeJob(job) {
12
+ if (running.has(job.id)) {
13
+ deps.logger.warn(`cron: job "${job.id}" already running, skipping`);
14
+ return;
15
+ }
16
+ running.add(job.id);
17
+ const title = `cron:${job.id}:${new Date().toISOString()}`;
18
+ deps.logger.info(`cron: firing job "${job.id}"`, { schedule: job.schedule });
19
+ try {
20
+ const session = await deps.client.session.create({
21
+ body: { title },
22
+ });
23
+ if (!session.data)
24
+ throw new Error("session.create returned no data");
25
+ const sessionId = session.data.id;
26
+ deps.logger.debug(`cron: job "${job.id}" session created`, { sessionId });
27
+ // session.prompt() is synchronous — blocks until the agent finishes.
28
+ // Wrap with AbortSignal timeout for safety.
29
+ const timeout = job.timeoutMs ?? deps.config.defaultTimeoutMs;
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), timeout);
32
+ let result;
33
+ try {
34
+ result = await deps.client.session.prompt({
35
+ path: { id: sessionId },
36
+ body: { parts: [{ type: "text", text: job.prompt }] },
37
+ });
38
+ }
39
+ catch (err) {
40
+ if (controller.signal.aborted) {
41
+ deps.logger.warn(`cron: job "${job.id}" timed out after ${timeout}ms`);
42
+ return;
43
+ }
44
+ throw err;
45
+ }
46
+ finally {
47
+ clearTimeout(timer);
48
+ }
49
+ if (!result.data) {
50
+ deps.logger.warn(`cron: job "${job.id}" returned no data`);
51
+ return;
52
+ }
53
+ const text = extractText(result.data.parts);
54
+ deps.logger.info(`cron: job "${job.id}" completed`, {
55
+ sessionId,
56
+ responseLength: text.length,
57
+ });
58
+ // Route result to channel if configured
59
+ if (job.reportTo && text.trim()) {
60
+ await deps.outbox.enqueue({
61
+ channel: job.reportTo.channel,
62
+ peerId: job.reportTo.peerId,
63
+ text,
64
+ threadId: job.reportTo.threadId,
65
+ });
66
+ deps.logger.info(`cron: job "${job.id}" result enqueued to ${job.reportTo.channel}:${job.reportTo.peerId}`);
67
+ }
68
+ }
69
+ catch (err) {
70
+ deps.logger.error(`cron: job "${job.id}" failed`, {
71
+ error: err instanceof Error ? err.message : String(err),
72
+ });
73
+ }
74
+ finally {
75
+ running.delete(job.id);
76
+ }
77
+ }
78
+ function start() {
79
+ if (!deps.config.enabled) {
80
+ deps.logger.info("cron: disabled by config");
81
+ return;
82
+ }
83
+ for (const job of deps.config.jobs) {
84
+ if (!job.enabled) {
85
+ deps.logger.info(`cron: skipping disabled job "${job.id}"`);
86
+ continue;
87
+ }
88
+ if (!cron.validate(job.schedule)) {
89
+ deps.logger.error(`cron: invalid schedule for job "${job.id}": ${job.schedule}`);
90
+ continue;
91
+ }
92
+ const task = cron.schedule(job.schedule, () => {
93
+ executeJob(job).catch((err) => {
94
+ deps.logger.error(`cron: unhandled error in job "${job.id}"`, {
95
+ error: err instanceof Error ? err.message : String(err),
96
+ });
97
+ });
98
+ });
99
+ jobs.set(job.id, { config: job, task });
100
+ deps.logger.info(`cron: scheduled "${job.id}" (${job.schedule}) — ${job.description}`);
101
+ }
102
+ deps.logger.info(`cron: ${jobs.size} job(s) scheduled`);
103
+ }
104
+ function stop() {
105
+ for (const [id, { task }] of jobs) {
106
+ task.stop();
107
+ deps.logger.debug(`cron: stopped job "${id}"`);
108
+ }
109
+ jobs.clear();
110
+ deps.logger.info("cron: all jobs stopped");
111
+ }
112
+ return { start, stop };
113
+ }
@@ -0,0 +1,9 @@
1
+ export { main } from "./index.js";
2
+ export type { Config, MemoryConfig, OutboxConfig, LogConfig, HealthConfig, RouterConfig, } from "./config/types.js";
3
+ export type { MemoryBackend, MemoryEntry, MemoryInput, MemorySearchOptions, MemoryStatus, MemoryCategory, } from "./memory/types.js";
4
+ export { createMemoryBackend } from "./memory/factory.js";
5
+ export type { ChannelAdapter, ChannelId, InboundMessage, OutboundMessage, ChannelStatus, } from "./channels/types.js";
6
+ export type { OutboxEntry, OutboxWriter } from "./outbox/writer.js";
7
+ export { createOutboxWriter } from "./outbox/writer.js";
8
+ export type { OutboxDrainer } from "./outbox/drainer.js";
9
+ export { createOutboxDrainer } from "./outbox/drainer.js";
@@ -0,0 +1,4 @@
1
+ export { main } from "./index.js";
2
+ export { createMemoryBackend } from "./memory/factory.js";
3
+ export { createOutboxWriter } from "./outbox/writer.js";
4
+ export { createOutboxDrainer } from "./outbox/drainer.js";
@@ -0,0 +1,17 @@
1
+ import type { ChannelAdapter, ChannelId } from "../channels/types.js";
2
+ import type { OutboxConfig } from "../config/types.js";
3
+ import type { MemoryBackend } from "../memory/types.js";
4
+ import type { Logger } from "../utils/logger.js";
5
+ type HealthDeps = {
6
+ port: number;
7
+ adapters: Map<ChannelId, ChannelAdapter>;
8
+ memory: MemoryBackend;
9
+ outbox: OutboxConfig;
10
+ logger: Logger;
11
+ };
12
+ export declare function createHealthServer(deps: HealthDeps): {
13
+ start: () => void;
14
+ stop: () => void;
15
+ };
16
+ export type HealthServer = ReturnType<typeof createHealthServer>;
17
+ export {};
@@ -0,0 +1,68 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { createHttpServer } from "../compat.js";
4
+ async function countFiles(dir) {
5
+ try {
6
+ const entries = await readdir(dir, { recursive: true });
7
+ return entries.filter((e) => e.endsWith(".json")).length;
8
+ }
9
+ catch {
10
+ return 0;
11
+ }
12
+ }
13
+ function channelsInfo(adapters) {
14
+ const result = {};
15
+ for (const [id, adapter] of adapters) {
16
+ result[id] = adapter.status();
17
+ }
18
+ return result;
19
+ }
20
+ export function createHealthServer(deps) {
21
+ let srv = null;
22
+ async function handleRequest(req) {
23
+ const url = new URL(req.url);
24
+ const json = (data, status = 200) => new Response(JSON.stringify(data, null, 2), {
25
+ status,
26
+ headers: { "content-type": "application/json" },
27
+ });
28
+ switch (url.pathname) {
29
+ case "/health": {
30
+ const channels = channelsInfo(deps.adapters);
31
+ const allConnected = Object.values(channels).every((s) => s === "connected");
32
+ const anyConnected = Object.values(channels).some((s) => s === "connected");
33
+ const status = allConnected ? "up" : anyConnected ? "degraded" : "down";
34
+ return json({ status, uptime: process.uptime() });
35
+ }
36
+ case "/channels": {
37
+ return json(channelsInfo(deps.adapters));
38
+ }
39
+ case "/memory": {
40
+ const info = await deps.memory.status();
41
+ return json(info);
42
+ }
43
+ case "/outbox": {
44
+ const pendingDir = deps.outbox.directory;
45
+ const deadDir = join(deps.outbox.directory, "dead");
46
+ const pending = await countFiles(pendingDir);
47
+ const dead = await countFiles(deadDir);
48
+ return json({ pending, dead });
49
+ }
50
+ default: {
51
+ return json({ error: "not found" }, 404);
52
+ }
53
+ }
54
+ }
55
+ function start() {
56
+ srv = createHttpServer(deps.port, handleRequest);
57
+ srv.start();
58
+ deps.logger.info("health: server started", { port: deps.port });
59
+ }
60
+ function stop() {
61
+ if (srv) {
62
+ srv.stop();
63
+ srv = null;
64
+ }
65
+ deps.logger.info("health: server stopped");
66
+ }
67
+ return { start, stop };
68
+ }
@@ -0,0 +1 @@
1
+ export declare function main(): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { createOpencode } from "@opencode-ai/sdk";
4
+ import { createRouter } from "./channels/router.js";
5
+ import { createSlackAdapter } from "./channels/slack.js";
6
+ import { createTelegramAdapter } from "./channels/telegram.js";
7
+ import { createWhatsAppAdapter } from "./channels/whatsapp.js";
8
+ import { loadConfig } from "./config/loader.js";
9
+ import { createCronScheduler } from "./cron/scheduler.js";
10
+ import { createHealthServer } from "./health/server.js";
11
+ import { createMemoryBackend } from "./memory/factory.js";
12
+ import { createOutboxDrainer } from "./outbox/drainer.js";
13
+ import { createOutboxWriter } from "./outbox/writer.js";
14
+ import { createSessionManager } from "./sessions/manager.js";
15
+ import { loadSessionMap } from "./sessions/persistence.js";
16
+ import { createLogger } from "./utils/logger.js";
17
+ import { onShutdown, setupShutdown } from "./utils/shutdown.js";
18
+ export async function main() {
19
+ const config = await loadConfig();
20
+ const logger = createLogger(config.log);
21
+ logger.info("opencode-claw starting", { version: "0.1.0" });
22
+ // --- Phase 3: Memory System ---
23
+ const memory = createMemoryBackend(config.memory);
24
+ await memory.initialize();
25
+ logger.info("memory: initialized", { backend: config.memory.backend });
26
+ onShutdown(async () => {
27
+ await memory.close();
28
+ });
29
+ const dir = dirname(fileURLToPath(import.meta.url));
30
+ const pluginPath = `file://${resolve(dir, "memory/plugin-entry.js")}`;
31
+ logger.info("opencode: starting server...", { plugins: [pluginPath] });
32
+ const { client, server } = await createOpencode({
33
+ port: config.opencode.port,
34
+ config: { plugin: [pluginPath] },
35
+ });
36
+ logger.info("opencode: server ready");
37
+ onShutdown(async () => {
38
+ logger.info("opencode: shutting down server");
39
+ server.close();
40
+ });
41
+ // Load persisted session map
42
+ const sessionMap = await loadSessionMap(config.sessions, logger);
43
+ const sessions = createSessionManager(client, config.sessions, sessionMap, logger);
44
+ onShutdown(async () => {
45
+ await sessions.persist();
46
+ });
47
+ // Setup graceful shutdown
48
+ setupShutdown(logger);
49
+ // --- Phase 2: Channel Adapters ---
50
+ const adapters = new Map();
51
+ if (config.channels.telegram?.enabled) {
52
+ const telegram = createTelegramAdapter(config.channels.telegram, logger);
53
+ adapters.set("telegram", telegram);
54
+ onShutdown(async () => {
55
+ await telegram.stop();
56
+ });
57
+ }
58
+ if (config.channels.slack?.enabled) {
59
+ const slack = createSlackAdapter(config.channels.slack, logger);
60
+ adapters.set("slack", slack);
61
+ onShutdown(async () => {
62
+ await slack.stop();
63
+ });
64
+ }
65
+ if (config.channels.whatsapp?.enabled) {
66
+ const whatsapp = createWhatsAppAdapter(config.channels.whatsapp, logger);
67
+ adapters.set("whatsapp", whatsapp);
68
+ onShutdown(async () => {
69
+ await whatsapp.stop();
70
+ });
71
+ }
72
+ const router = createRouter({
73
+ client,
74
+ sessions,
75
+ adapters,
76
+ config,
77
+ logger,
78
+ timeoutMs: config.router.timeoutMs,
79
+ });
80
+ // Start all adapters with the router handler
81
+ for (const [id, adapter] of adapters) {
82
+ logger.info(`channel: starting ${id}`);
83
+ await adapter.start(router.handler);
84
+ }
85
+ // --- Phase 4: Outbox ---
86
+ const outbox = createOutboxWriter(config.outbox);
87
+ const drainer = createOutboxDrainer(config.outbox, adapters, logger);
88
+ drainer.start();
89
+ onShutdown(() => {
90
+ drainer.stop();
91
+ });
92
+ // --- Phase 5: Cron ---
93
+ if (config.cron?.enabled) {
94
+ const scheduler = createCronScheduler({
95
+ client,
96
+ outbox,
97
+ config: config.cron,
98
+ logger,
99
+ });
100
+ scheduler.start();
101
+ onShutdown(() => {
102
+ scheduler.stop();
103
+ });
104
+ }
105
+ // --- Phase 7: Health Server ---
106
+ if (config.health?.enabled) {
107
+ const health = createHealthServer({
108
+ port: config.health.port,
109
+ adapters,
110
+ memory,
111
+ outbox: config.outbox,
112
+ logger,
113
+ });
114
+ health.start();
115
+ onShutdown(() => {
116
+ health.stop();
117
+ });
118
+ }
119
+ logger.info("opencode-claw ready", {
120
+ channels: Object.entries(config.channels)
121
+ .filter(([_, v]) => v?.enabled)
122
+ .map(([k]) => k),
123
+ memory: config.memory.backend,
124
+ cron: config.cron?.enabled ?? false,
125
+ health: config.health?.enabled ?? false,
126
+ });
127
+ }
@@ -0,0 +1,3 @@
1
+ import type { MemoryConfig } from "../config/types.js";
2
+ import type { MemoryBackend } from "./types.js";
3
+ export declare function createMemoryBackend(config: MemoryConfig): MemoryBackend;
@@ -0,0 +1,14 @@
1
+ import { createOpenVikingBackend } from "./openviking.js";
2
+ import { createTxtMemoryBackend } from "./txt.js";
3
+ export function createMemoryBackend(config) {
4
+ if (config.backend === "txt") {
5
+ return createTxtMemoryBackend(config.txt.directory);
6
+ }
7
+ if (config.backend === "openviking") {
8
+ if (!config.openviking) {
9
+ throw new Error("memory.openviking config required when backend is 'openviking'");
10
+ }
11
+ return createOpenVikingBackend(config.openviking, config.txt.directory);
12
+ }
13
+ throw new Error(`Unknown memory backend: ${config.backend}`);
14
+ }
@@ -0,0 +1,5 @@
1
+ import type { MemoryConfig } from "../config/types.js";
2
+ import type { MemoryBackend } from "./types.js";
3
+ type OpenVikingConfig = NonNullable<MemoryConfig["openviking"]>;
4
+ export declare function createOpenVikingBackend(config: OpenVikingConfig, fallbackDir?: string): MemoryBackend;
5
+ export {};
@@ -0,0 +1,138 @@
1
+ const CATEGORY_TO_PATH = {
2
+ project: "patterns",
3
+ experience: "cases",
4
+ preference: "preferences",
5
+ entity: "entities",
6
+ event: "events",
7
+ knowledge: "patterns",
8
+ };
9
+ const PATH_TO_CATEGORY = {
10
+ patterns: "project",
11
+ cases: "experience",
12
+ preferences: "preference",
13
+ entities: "entity",
14
+ events: "event",
15
+ };
16
+ function mapCategory(category) {
17
+ return CATEGORY_TO_PATH[category] ?? "patterns";
18
+ }
19
+ function reverseCategory(path) {
20
+ for (const [segment, cat] of Object.entries(PATH_TO_CATEGORY)) {
21
+ if (path.includes(segment))
22
+ return cat;
23
+ }
24
+ return "knowledge";
25
+ }
26
+ async function request(url, method, body, params) {
27
+ let endpoint = url;
28
+ if (params) {
29
+ const qs = new URLSearchParams(params).toString();
30
+ if (qs)
31
+ endpoint = `${url}?${qs}`;
32
+ }
33
+ const init = {
34
+ method,
35
+ headers: { "Content-Type": "application/json" },
36
+ };
37
+ if (body !== undefined) {
38
+ init.body = JSON.stringify(body);
39
+ }
40
+ const res = await fetch(endpoint, init);
41
+ const data = (await res.json());
42
+ if (data.status === "error") {
43
+ const msg = data.error?.message ?? "Unknown OpenViking error";
44
+ throw new Error(`OpenViking: ${data.error?.code ?? "UNKNOWN"} — ${msg}`);
45
+ }
46
+ return data.result;
47
+ }
48
+ export function createOpenVikingBackend(config, fallbackDir) {
49
+ const base = config.url.replace(/\/$/, "");
50
+ let available = false;
51
+ let fallback;
52
+ return {
53
+ async initialize() {
54
+ try {
55
+ await request(`${base}/api/v1/sessions`, "GET");
56
+ available = true;
57
+ }
58
+ catch {
59
+ if (config.fallback && fallbackDir) {
60
+ const { createTxtMemoryBackend } = await import("./txt.js");
61
+ fallback = createTxtMemoryBackend(fallbackDir);
62
+ await fallback.initialize();
63
+ available = false;
64
+ }
65
+ else {
66
+ throw new Error(`OpenViking unavailable at ${base} and fallback is disabled`);
67
+ }
68
+ }
69
+ },
70
+ async search(query, options) {
71
+ if (fallback)
72
+ return fallback.search(query, options);
73
+ const result = await request(`${base}/api/v1/search/find`, "POST", {
74
+ query,
75
+ limit: options?.limit ?? 10,
76
+ score_threshold: options?.minRelevance ?? 0.1,
77
+ });
78
+ const items = result.items ?? [];
79
+ return items.map((item) => ({
80
+ id: item.uri,
81
+ content: item.content,
82
+ category: reverseCategory(item.uri),
83
+ source: "openviking",
84
+ createdAt: new Date(),
85
+ relevance: item.score,
86
+ metadata: item.metadata,
87
+ }));
88
+ },
89
+ async store(entry) {
90
+ if (fallback)
91
+ return fallback.store(entry);
92
+ const session = await request(`${base}/api/v1/sessions`, "POST", {});
93
+ const sid = session.session_id;
94
+ const category = mapCategory(entry.category);
95
+ const tagged = `[${category}] [source:${entry.source}] ${entry.content}`;
96
+ await request(`${base}/api/v1/sessions/${sid}/messages`, "POST", {
97
+ role: "user",
98
+ content: tagged,
99
+ });
100
+ await request(`${base}/api/v1/sessions/${sid}/commit`, "POST");
101
+ },
102
+ async delete(id) {
103
+ if (fallback)
104
+ return fallback.delete(id);
105
+ if (id.startsWith("viking://")) {
106
+ await request(`${base}/api/v1/fs`, "DELETE", undefined, {
107
+ uri: id,
108
+ recursive: "false",
109
+ });
110
+ }
111
+ },
112
+ async status() {
113
+ if (fallback) {
114
+ const s = await fallback.status();
115
+ return { ...s, backend: "openviking (fallback: txt)" };
116
+ }
117
+ try {
118
+ const sessions = await request(`${base}/api/v1/sessions`, "GET");
119
+ return {
120
+ backend: "openviking",
121
+ initialized: true,
122
+ entryCount: Array.isArray(sessions) ? sessions.length : 0,
123
+ };
124
+ }
125
+ catch {
126
+ return {
127
+ backend: "openviking",
128
+ initialized: available,
129
+ entryCount: 0,
130
+ };
131
+ }
132
+ },
133
+ async close() {
134
+ if (fallback)
135
+ await fallback.close();
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ declare const memoryPlugin: Plugin;
3
+ export { memoryPlugin };
@@ -0,0 +1,11 @@
1
+ import { loadConfig } from "../config/loader.js";
2
+ import { createMemoryBackend } from "./factory.js";
3
+ import { createMemoryPlugin } from "./plugin.js";
4
+ // This file runs inside OpenCode's child process (loaded via `import()`).
5
+ // It reads the same opencode-claw config and creates its own MemoryBackend
6
+ // instance pointing to the same MEMORY.md file on disk.
7
+ const config = await loadConfig();
8
+ const backend = createMemoryBackend(config.memory);
9
+ await backend.initialize();
10
+ const memoryPlugin = createMemoryPlugin(backend);
11
+ export { memoryPlugin };
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import type { MemoryBackend, MemoryCategory } from "./types.js";
3
+ declare function createMemoryPlugin(backend: MemoryBackend): Plugin;
4
+ export { createMemoryPlugin };
5
+ export type { MemoryBackend, MemoryCategory };
@@ -0,0 +1,63 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ const z = tool.schema;
3
+ const CATEGORIES = ["project", "experience", "preference", "entity", "event", "knowledge"];
4
+ function createMemoryPlugin(backend) {
5
+ return async () => ({
6
+ tool: {
7
+ memory_search: tool({
8
+ description: "Search long-term memory for relevant context about projects, experiences, preferences, or entities",
9
+ args: {
10
+ query: z.string().describe("What to search for"),
11
+ category: z
12
+ .enum(CATEGORIES)
13
+ .optional()
14
+ .describe("Filter by category: project, experience, preference, entity, event, knowledge"),
15
+ limit: z
16
+ .number()
17
+ .int()
18
+ .min(1)
19
+ .max(20)
20
+ .optional()
21
+ .describe("Max results to return (default: 5)"),
22
+ },
23
+ execute: async ({ query, category, limit }) => {
24
+ const results = await backend.search(query, {
25
+ category: category,
26
+ limit: limit ?? 5,
27
+ });
28
+ if (results.length === 0)
29
+ return "No relevant memories found.";
30
+ return results.map((r) => `[${r.category}] ${r.content}`).join("\n\n---\n\n");
31
+ },
32
+ }),
33
+ memory_store: tool({
34
+ description: "Store important information in long-term memory for future sessions",
35
+ args: {
36
+ content: z.string().describe("The information to remember"),
37
+ category: z
38
+ .enum(CATEGORIES)
39
+ .describe("Category: project, experience, preference, entity, event, knowledge"),
40
+ },
41
+ execute: async ({ content, category }) => {
42
+ await backend.store({
43
+ content: content,
44
+ category: category,
45
+ source: "agent",
46
+ });
47
+ return "Stored in memory.";
48
+ },
49
+ }),
50
+ },
51
+ "experimental.chat.system.transform": async (_input, output) => {
52
+ const memories = await backend.search("recent context", {
53
+ limit: 5,
54
+ minRelevance: 0.05,
55
+ });
56
+ if (memories.length === 0)
57
+ return;
58
+ const block = memories.map((m) => `- [${m.category}] ${m.content}`).join("\n");
59
+ output.system.push(`\n\n## Relevant Context from Memory\n${block}`);
60
+ },
61
+ });
62
+ }
63
+ export { createMemoryPlugin };
@@ -0,0 +1,2 @@
1
+ import type { MemoryBackend } from "./types.js";
2
+ export declare function createTxtMemoryBackend(directory: string): MemoryBackend;