opencodekit 0.17.13 → 0.18.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 (36) hide show
  1. package/dist/index.js +4 -6
  2. package/dist/template/.opencode/dcp.jsonc +81 -81
  3. package/dist/template/.opencode/memory/memory.db +0 -0
  4. package/dist/template/.opencode/memory.db +0 -0
  5. package/dist/template/.opencode/memory.db-shm +0 -0
  6. package/dist/template/.opencode/memory.db-wal +0 -0
  7. package/dist/template/.opencode/opencode.json +199 -23
  8. package/dist/template/.opencode/opencode.json.tui-migration.bak +1380 -0
  9. package/dist/template/.opencode/package.json +1 -1
  10. package/dist/template/.opencode/plugin/lib/capture.ts +177 -0
  11. package/dist/template/.opencode/plugin/lib/context.ts +194 -0
  12. package/dist/template/.opencode/plugin/lib/curator.ts +234 -0
  13. package/dist/template/.opencode/plugin/lib/db/maintenance.ts +312 -0
  14. package/dist/template/.opencode/plugin/lib/db/observations.ts +299 -0
  15. package/dist/template/.opencode/plugin/lib/db/pipeline.ts +520 -0
  16. package/dist/template/.opencode/plugin/lib/db/schema.ts +356 -0
  17. package/dist/template/.opencode/plugin/lib/db/types.ts +211 -0
  18. package/dist/template/.opencode/plugin/lib/distill.ts +376 -0
  19. package/dist/template/.opencode/plugin/lib/inject.ts +126 -0
  20. package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +188 -0
  21. package/dist/template/.opencode/plugin/lib/memory-db.ts +54 -936
  22. package/dist/template/.opencode/plugin/lib/memory-helpers.ts +202 -0
  23. package/dist/template/.opencode/plugin/lib/memory-hooks.ts +240 -0
  24. package/dist/template/.opencode/plugin/lib/memory-tools.ts +341 -0
  25. package/dist/template/.opencode/plugin/memory.ts +56 -60
  26. package/dist/template/.opencode/plugin/sessions.ts +372 -93
  27. package/dist/template/.opencode/tui.json +15 -0
  28. package/package.json +1 -1
  29. package/dist/template/.opencode/tool/action-queue.ts +0 -313
  30. package/dist/template/.opencode/tool/memory-admin.ts +0 -445
  31. package/dist/template/.opencode/tool/memory-get.ts +0 -143
  32. package/dist/template/.opencode/tool/memory-read.ts +0 -45
  33. package/dist/template/.opencode/tool/memory-search.ts +0 -264
  34. package/dist/template/.opencode/tool/memory-timeline.ts +0 -105
  35. package/dist/template/.opencode/tool/memory-update.ts +0 -63
  36. package/dist/template/.opencode/tool/observation.ts +0 -357
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Memory Plugin — Core Tools
3
+ *
4
+ * observation, memory-search, memory-get, memory-read, memory-update, memory-timeline
5
+ *
6
+ * Uses factory pattern: createCoreTools(deps) returns tool definitions
7
+ * that can be spread into plugin's tool:{} export.
8
+ */
9
+
10
+ import { readdir } from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { tool } from "@opencode-ai/plugin/tool";
13
+ import {
14
+ type ConfidenceLevel,
15
+ checkFTS5Available,
16
+ getMemoryFile,
17
+ getObservationsByIds,
18
+ getTimelineAroundObservation,
19
+ type ObservationSource,
20
+ type ObservationType,
21
+ searchDistillationsFTS,
22
+ searchObservationsFTS,
23
+ storeObservation,
24
+ upsertMemoryFile,
25
+ } from "./memory-db.js";
26
+ import {
27
+ autoDetectFiles,
28
+ formatObservation,
29
+ parseCSV,
30
+ safeReadFile,
31
+ TYPE_ICONS,
32
+ VALID_TYPES,
33
+ } from "./memory-helpers.js";
34
+
35
+ interface CoreToolDeps {
36
+ handoffDir: string;
37
+ }
38
+
39
+ export function createCoreTools(deps: CoreToolDeps) {
40
+ const { handoffDir } = deps;
41
+
42
+ return {
43
+ observation: tool({
44
+ description: `Create a structured observation for future reference.\n\t\n\tPurpose:\n\t- Capture decisions, bugs, features, patterns, discoveries, learnings, or warnings\n\t- Auto-detects file references from content (file:line, \`path\`, src/, .opencode/)\n\t- Stores in SQLite with FTS5 index for fast search\n\t- Supports enhanced schema: facts, subtitle, files_read/files_modified\n\t\n\tConfidence guidance:\n\t- high: verified by tests, logs, or direct inspection (default)\n\t- medium: likely, but not fully verified\n\t- low: uncertain or speculative\n\t\n\tType-specific examples:\n\tdecision\n\tobservation({\n\t type: "decision",\n\t title: "Use JWT for auth",\n\t narrative: "Chose JWT for stateless auth across services.",\n\t facts: "stateless, scalable",\n\t concepts: "authentication, jwt",\n\t confidence: "high"\n\t})\n\t\n\tbugfix\n\tobservation({\n\t type: "bugfix",\n\t title: "Fix null pointer on login",\n\t narrative: "Guarded optional user in src/auth.ts:42 to prevent crash.",\n\t files_modified: "src/auth.ts",\n\t concepts: "auth, null-check",\n\t confidence: "high"\n\t})\n\t\n\tfeature\n\tobservation({\n\t type: "feature",\n\t title: "Add CLI --dry-run",\n\t narrative: "Introduce dry-run mode to show planned changes without writing.",\n\t files_modified: "src/commands/init.ts",\n\t concepts: "cli, ux",\n\t confidence: "medium"\n\t})\n\t\n\tpattern\n\tobservation({\n\t type: "pattern",\n\t title: "Use zod for input validation",\n\t narrative: "All command inputs validated with zod schemas before execute.",\n\t concepts: "validation, zod",\n\t confidence: "high"\n\t})\n\t\n\tdiscovery\n\tobservation({\n\t type: "discovery",\n\t title: "Build copies .opencode/ to dist/template/",\n\t narrative: "Found rsync step in build.ts that bundles .opencode/.",\n\t files_read: "build.ts",\n\t confidence: "high"\n\t})\n\t\n\tlearning\n\tobservation({\n\t type: "learning",\n\t title: "Bun test respects --watch",\n\t narrative: "Observed bun test --watch keeps runner active during edits.",\n\t confidence: "medium"\n\t})\n\t\n\twarning\n\tobservation({\n\t type: "warning",\n\t title: "Do not edit dist/ directly",\n\t narrative: "dist/ is built output and overwritten on build.",\n\t concepts: "build, generated",\n\t confidence: "high"\n\t})`,
45
+ args: {
46
+ type: tool.schema
47
+ .string()
48
+ .describe(
49
+ "Observation type: decision, bugfix, feature, pattern, discovery, learning, warning",
50
+ ),
51
+ title: tool.schema.string().describe("Brief title"),
52
+ subtitle: tool.schema.string().optional().describe("Optional subtitle"),
53
+ facts: tool.schema
54
+ .string()
55
+ .optional()
56
+ .describe("Comma-separated key facts"),
57
+ narrative: tool.schema.string().optional().describe("Detailed content"),
58
+ content: tool.schema
59
+ .string()
60
+ .optional()
61
+ .describe("DEPRECATED: Use 'narrative'"),
62
+ concepts: tool.schema
63
+ .string()
64
+ .optional()
65
+ .describe("Comma-separated concept tags"),
66
+ files_read: tool.schema
67
+ .string()
68
+ .optional()
69
+ .describe("Comma-separated files read"),
70
+ files_modified: tool.schema
71
+ .string()
72
+ .optional()
73
+ .describe("Comma-separated files modified"),
74
+ files: tool.schema
75
+ .string()
76
+ .optional()
77
+ .describe("DEPRECATED: Use 'files_modified'"),
78
+ bead_id: tool.schema.string().optional().describe("Related bead ID"),
79
+ confidence: tool.schema
80
+ .string()
81
+ .optional()
82
+ .describe("high, medium, low"),
83
+ supersedes: tool.schema
84
+ .string()
85
+ .optional()
86
+ .describe("ID this supersedes"),
87
+ source: tool.schema
88
+ .string()
89
+ .optional()
90
+ .describe("manual, curator, imported"),
91
+ },
92
+ execute: async (args) => {
93
+ const obsType = args.type as ObservationType;
94
+ if (!VALID_TYPES.includes(obsType)) {
95
+ return `Error: Invalid type "${args.type}". Valid: ${VALID_TYPES.join(", ")}`;
96
+ }
97
+
98
+ const confidence = (args.confidence ?? "high") as ConfidenceLevel;
99
+ if (!["high", "medium", "low"].includes(confidence)) {
100
+ return `Error: Invalid confidence "${args.confidence}". Valid: high, medium, low`;
101
+ }
102
+
103
+ const narrative = args.narrative ?? args.content;
104
+ const filesModifiedRaw = args.files_modified ?? args.files;
105
+ const facts = parseCSV(args.facts);
106
+ const concepts = parseCSV(args.concepts);
107
+ let filesRead = parseCSV(args.files_read);
108
+ const filesModified = parseCSV(filesModifiedRaw);
109
+
110
+ if (narrative) {
111
+ const detected = autoDetectFiles(narrative);
112
+ if (detected.length > 0) {
113
+ const existing = new Set([
114
+ ...(filesRead ?? []),
115
+ ...(filesModified ?? []),
116
+ ]);
117
+ const newRefs = detected.filter((f) => !existing.has(f));
118
+ if (newRefs.length > 0)
119
+ filesRead = [...(filesRead ?? []), ...newRefs];
120
+ }
121
+ }
122
+
123
+ let supersedes: number | undefined;
124
+ if (args.supersedes) {
125
+ const parsed = Number.parseInt(args.supersedes, 10);
126
+ if (!Number.isNaN(parsed)) supersedes = parsed;
127
+ }
128
+
129
+ const source = (args.source ?? "manual") as ObservationSource;
130
+
131
+ const id = storeObservation({
132
+ type: obsType,
133
+ title: args.title,
134
+ subtitle: args.subtitle,
135
+ facts,
136
+ narrative,
137
+ concepts,
138
+ files_read: filesRead,
139
+ files_modified: filesModified,
140
+ confidence,
141
+ bead_id: args.bead_id,
142
+ supersedes,
143
+ source,
144
+ });
145
+
146
+ return `${TYPE_ICONS[obsType] ?? "\uD83D\uDCCC"} Observation #${id} stored [${obsType}] "${args.title}" (confidence: ${confidence}, source: ${source})`;
147
+ },
148
+ }),
149
+
150
+ "memory-search": tool({
151
+ description: `Search memory across observations and markdown archives.\n\t\n\tPurpose:\n\t- Fast, ranked search across all observations in SQLite (when FTS5 is available)\n\t- Returns compact index (~50-100 tokens per result) for progressive disclosure\n\t- Use memory-get for full details after identifying relevant observations\n\t\n\tFTS5 availability:\n\t- Auto-detected at runtime; if unavailable, observation searches fall back to file scan\n\t\n\tSearch modes and hints:\n\t- "observations" (default): Best for decisions, bugs, learnings; uses FTS5 ranking when available\n\t- "handoffs": Use for past session handoffs and summaries\n\t- "research": Use for research notes and external findings\n\t- "templates": Use for memory templates and boilerplate references\n\t- "beads": Use for task artifacts in .beads/artifacts\n\t- "all": Use when you are unsure where info lives; searches SQLite + markdown + beads\n\t\n\tExample:\n\tmemory-search({ query: "authentication" })\n\tmemory-search({ query: "auth", type: "decision", limit: 5 })`,
152
+ args: {
153
+ query: tool.schema.string().describe("Search query"),
154
+ type: tool.schema
155
+ .string()
156
+ .optional()
157
+ .describe("Filter by type or scope"),
158
+ limit: tool.schema
159
+ .number()
160
+ .optional()
161
+ .describe("Max results (default: 10)"),
162
+ },
163
+ execute: async (args) => {
164
+ const query = args.query.trim();
165
+ if (!query) return "Error: Empty search query";
166
+ const limit = args.limit ?? 10;
167
+ const scope = args.type ?? "observations";
168
+ const lines: string[] = [];
169
+
170
+ if (
171
+ scope === "observations" ||
172
+ scope === "all" ||
173
+ VALID_TYPES.includes(scope as ObservationType)
174
+ ) {
175
+ const typeFilter = VALID_TYPES.includes(scope as ObservationType)
176
+ ? (scope as ObservationType)
177
+ : undefined;
178
+ if (checkFTS5Available()) {
179
+ const results = searchObservationsFTS(query, {
180
+ type: typeFilter,
181
+ limit,
182
+ });
183
+ if (results.length > 0) {
184
+ lines.push(`## Observations (${results.length} results)\n`);
185
+ lines.push("| ID | Type | Title | Date |");
186
+ lines.push("|---|---|---|---|");
187
+ for (const r of results)
188
+ lines.push(
189
+ `| ${r.id} | ${r.type} | ${r.title} | ${r.created_at.slice(0, 10)} |`,
190
+ );
191
+ lines.push("");
192
+ for (const r of results.slice(0, 3)) {
193
+ if (r.snippet) lines.push(`**#${r.id}**: ${r.snippet}`);
194
+ }
195
+ lines.push(
196
+ `\nUse \`memory-get({ ids: "${results
197
+ .slice(0, 3)
198
+ .map((r) => r.id)
199
+ .join(",")}" })\` for full details.`,
200
+ );
201
+ } else {
202
+ lines.push("No observation matches found.");
203
+ }
204
+ } else {
205
+ lines.push("FTS5 not available. Use memory-admin to check status.");
206
+ }
207
+ }
208
+
209
+ if (scope === "distillations" || scope === "all") {
210
+ const distResults = searchDistillationsFTS(query, limit);
211
+ if (distResults.length > 0) {
212
+ lines.push(`\n## Distillations (${distResults.length} results)\n`);
213
+ for (const d of distResults) {
214
+ lines.push(
215
+ `- **Session ${d.session_id.slice(0, 8)}** (${d.message_count} msgs): ${d.snippet}`,
216
+ );
217
+ }
218
+ }
219
+ }
220
+
221
+ if (scope === "handoffs" || scope === "all") {
222
+ try {
223
+ const handoffFiles = await readdir(handoffDir);
224
+ const matches: string[] = [];
225
+ for (const f of handoffFiles.filter((n) => n.endsWith(".md"))) {
226
+ const content = await safeReadFile(path.join(handoffDir, f));
227
+ if (content.toLowerCase().includes(query.toLowerCase()))
228
+ matches.push(f);
229
+ }
230
+ if (matches.length > 0) {
231
+ lines.push(`\n## Handoffs (${matches.length} matches)\n`);
232
+ for (const m of matches.slice(0, 5)) lines.push(`- ${m}`);
233
+ }
234
+ } catch {
235
+ /* No handoffs directory */
236
+ }
237
+ }
238
+
239
+ return lines.length > 0 ? lines.join("\n") : "No results found.";
240
+ },
241
+ }),
242
+
243
+ "memory-get": tool({
244
+ description: `Get full observation details by ID.\n\t\n\tPurpose:\n\t- Progressive disclosure: fetch full details after identifying relevant observations via search\n\t- Get complete narrative, facts, and metadata\n\t- Supports multiple IDs for batch retrieval\n\t\n\tExample:\n\tmemory-get({ ids: "42" }) // Single observation\n\tmemory-get({ ids: "1,5,10" }) // Multiple observations`,
245
+ args: {
246
+ ids: tool.schema.string().describe("Comma-separated observation IDs"),
247
+ },
248
+ execute: async (args) => {
249
+ const idList = args.ids
250
+ .split(",")
251
+ .map((s) => Number.parseInt(s.trim(), 10))
252
+ .filter((n) => !Number.isNaN(n));
253
+ if (idList.length === 0) return "Error: No valid IDs provided";
254
+ const observations = getObservationsByIds(idList);
255
+ if (observations.length === 0)
256
+ return "No observations found for given IDs.";
257
+ return observations
258
+ .map((obs) => formatObservation(obs))
259
+ .join("\n\n---\n\n");
260
+ },
261
+ }),
262
+
263
+ "memory-read": tool({
264
+ description: `Read memory files for persistent cross-session context.\n\t\n\tPurpose:\n\t- Retrieve project state, learnings, and active tasks\n\t- Reads from SQLite database\n\t- Supports subdirectories: handoffs/, research/\n\t\n\tExample:\n\tmemory-read({ file: "handoffs/2024-01-20-phase-1" })\n\tmemory-read({ file: "research/2024-01-topic" })`,
265
+ args: {
266
+ file: tool.schema.string().optional().describe("Memory file path"),
267
+ },
268
+ execute: async (args) => {
269
+ const filePath = (args.file ?? "").replace(/\.md$/, "");
270
+ if (!filePath) return "Error: No file path provided";
271
+ const row = getMemoryFile(filePath);
272
+ return row ? row.content : `Memory file "${filePath}" not found.`;
273
+ },
274
+ }),
275
+
276
+ "memory-update": tool({
277
+ description: `Update memory files with new learnings, progress, or context.\n\t\n\tPurpose:\n\t- Write or append to project memory in SQLite\n\t- Supports subdirectories (e.g., 'research/2024-01-topic')\n\t- Two modes: 'replace' (overwrite) or 'append' (add to end)\n\t\n\tExample:\n\tmemory-update({ file: "research/session-findings", content: "..." })\n\tmemory-update({ file: "handoffs/phase-2", content: "...", mode: "append" })`,
278
+ args: {
279
+ file: tool.schema.string().describe("Memory file to update"),
280
+ content: tool.schema.string().describe("Content to write or append"),
281
+ mode: tool.schema
282
+ .string()
283
+ .optional()
284
+ .describe("replace (default) or append"),
285
+ },
286
+ execute: async (args) => {
287
+ const filePath = args.file.replace(/\.md$/, "");
288
+ const mode = (args.mode ?? "replace") as "replace" | "append";
289
+ let finalContent = args.content;
290
+ if (mode === "append") {
291
+ finalContent = `\n---\n_Updated: ${new Date().toISOString()}_\n\n${args.content}`;
292
+ }
293
+ upsertMemoryFile(filePath, finalContent, mode);
294
+ return `Memory file "${filePath}" updated (mode: ${mode}).`;
295
+ },
296
+ }),
297
+
298
+ "memory-timeline": tool({
299
+ description: `Get chronological context around an observation.\n\t\n\tPurpose:\n\t- Progressive disclosure: see what was happening before/after a specific observation\n\t- Understand decision context over time\n\t- Navigate memory timeline\n\t\n\tExample:\n\tmemory-timeline({ anchor_id: 42, depth_before: 5, depth_after: 5 })`,
300
+ args: {
301
+ anchor_id: tool.schema.number().describe("ID of the observation"),
302
+ depth_before: tool.schema
303
+ .number()
304
+ .optional()
305
+ .describe("Earlier observations (default: 5)"),
306
+ depth_after: tool.schema
307
+ .number()
308
+ .optional()
309
+ .describe("Later observations (default: 5)"),
310
+ },
311
+ execute: async (args) => {
312
+ const { anchor, before, after } = getTimelineAroundObservation(
313
+ args.anchor_id,
314
+ args.depth_before ?? 5,
315
+ args.depth_after ?? 5,
316
+ );
317
+ if (!anchor) return `Observation #${args.anchor_id} not found.`;
318
+ const lines: string[] = [];
319
+ if (before.length > 0) {
320
+ lines.push("### Earlier");
321
+ for (const b of before)
322
+ lines.push(
323
+ ` ${b.id}: [${b.type}] ${b.title} (${b.created_at.slice(0, 10)})`,
324
+ );
325
+ }
326
+ lines.push(
327
+ `\n### ${TYPE_ICONS[anchor.type] ?? "\uD83D\uDCCC"} Current: #${anchor.id} [${anchor.type}] ${anchor.title}`,
328
+ );
329
+ if (anchor.narrative) lines.push(anchor.narrative.slice(0, 200));
330
+ if (after.length > 0) {
331
+ lines.push("\n### Later");
332
+ for (const a of after)
333
+ lines.push(
334
+ ` ${a.id}: [${a.type}] ${a.title} (${a.created_at.slice(0, 10)})`,
335
+ );
336
+ }
337
+ return lines.join("\n");
338
+ },
339
+ }),
340
+ };
341
+ }
@@ -1,27 +1,50 @@
1
1
  /**
2
- * Memory Plugin — Database Maintenance & UX Notifications
2
+ * Unified Memory Plugin v2
3
3
  *
4
- * Handles infrastructure tasks that the agent shouldn't manage:
5
- * 1. FTS5 index optimization on session idle
6
- * 2. WAL checkpoint when file gets large
7
- * 3. Toast notification when observations are saved
8
- * 4. Toast warning on session errors
4
+ * Consolidates all memory tools + hooks + compaction into a single plugin.
5
+ *
6
+ * Systems:
7
+ * 1. Capture message.updated temporal_messages
8
+ * 2. Distillation session.idle → TF-IDF compression
9
+ * 3. Curator — session.idle → pattern-matched observations
10
+ * 4. LTM Injection — system.transform → relevance-scored knowledge
11
+ * 5. Context Management — messages.transform → token budget enforcement
12
+ *
13
+ * Tools: observation, memory-search, memory-get, memory-read,
14
+ * memory-update, memory-timeline, memory-admin
15
+ *
16
+ * Module structure:
17
+ * memory.ts — Plugin entry (this file)
18
+ * lib/memory-helpers.ts — Constants, compaction utilities, formatting
19
+ * lib/memory-tools.ts — Core tools (observation, search, get, read, update, timeline)
20
+ * lib/memory-admin-tools.ts — Admin tools (memory-admin)
21
+ * lib/memory-hooks.ts — Event hooks, transforms, compaction
22
+ * lib/memory-db.ts — Database barrel (re-exports from lib/db/*.ts)
23
+ * lib/capture.ts — Message capture module
24
+ * lib/distill.ts — Heuristic distillation engine
25
+ * lib/curator.ts — Pattern-based knowledge extraction
26
+ * lib/inject.ts — LTM relevance scoring + injection
27
+ * lib/context.ts — Context window management
9
28
  */
10
29
 
30
+ import path from "node:path";
11
31
  import type { Plugin } from "@opencode-ai/plugin";
12
- import {
13
- checkFTS5Available,
14
- checkpointWAL,
15
- getDatabaseSizes,
16
- optimizeFTS5,
17
- } from "./lib/memory-db.js";
32
+ import { createAdminTools } from "./lib/memory-admin-tools.js";
33
+ import { createHooks } from "./lib/memory-hooks.js";
34
+ import { createCoreTools } from "./lib/memory-tools.js";
35
+
36
+ // ============================================================================
37
+ // Plugin Export
38
+ // ============================================================================
39
+
40
+ export const MemoryPlugin: Plugin = async ({ client, directory }) => {
41
+ const memoryDir = path.join(directory, ".opencode", "memory");
42
+ const handoffDir = path.join(memoryDir, "handoffs");
18
43
 
19
- export const MemoryPlugin: Plugin = async ({ client }) => {
44
+ // Logging + toast helpers
20
45
  const log = async (message: string, level: "info" | "warn" = "info") => {
21
46
  await client.app
22
- .log({
23
- body: { service: "memory", level, message },
24
- })
47
+ .log({ body: { service: "memory", level, message } })
25
48
  .catch(() => {});
26
49
  };
27
50
 
@@ -40,56 +63,29 @@ export const MemoryPlugin: Plugin = async ({ client }) => {
40
63
  },
41
64
  });
42
65
  } catch {
43
- // Toast API unavailable, continue silently
66
+ /* Toast API unavailable */
44
67
  }
45
68
  };
46
69
 
47
- return {
48
- // FTS5 optimization + WAL checkpoint on session idle
49
- "session.idle": async () => {
50
- // Optimize FTS5 index for fast search
51
- try {
52
- if (checkFTS5Available()) {
53
- optimizeFTS5();
54
- await log("FTS5 index optimized");
55
- }
56
- } catch (err) {
57
- const msg = err instanceof Error ? err.message : String(err);
58
- await log(`FTS5 optimization failed: ${msg}`, "warn");
59
- }
70
+ // Assemble tools
71
+ const coreTools = createCoreTools({ handoffDir });
72
+ const adminTools = createAdminTools({ directory });
60
73
 
61
- // Checkpoint WAL if it's grown past 1MB
62
- try {
63
- const sizes = getDatabaseSizes();
64
- if (sizes.wal > 1024 * 1024) {
65
- const result = checkpointWAL();
66
- if (result.checkpointed) {
67
- await log(
68
- `WAL checkpointed (was ${Math.round(sizes.wal / 1024)}KB)`,
69
- );
70
- }
71
- }
72
- } catch (err) {
73
- const msg = err instanceof Error ? err.message : String(err);
74
- await log(`WAL checkpoint failed: ${msg}`, "warn");
75
- }
76
- },
74
+ // Assemble hooks
75
+ const hooks = createHooks({
76
+ memoryDir,
77
+ handoffDir,
78
+ directory,
79
+ showToast,
80
+ log,
81
+ });
77
82
 
78
- // Toast when an observation is saved
79
- "tool.execute.after": async (input, _output) => {
80
- if (input.tool === "observation") {
81
- await showToast("Saved", "Observation added to memory");
82
- }
83
- },
84
-
85
- // Warn on session errors
86
- "session.error": async () => {
87
- await showToast(
88
- "Session Error",
89
- "Consider saving important learnings with observation tool",
90
- "warning",
91
- );
83
+ return {
84
+ tool: {
85
+ ...coreTools,
86
+ ...adminTools,
92
87
  },
88
+ ...hooks,
93
89
  };
94
90
  };
95
91