@lotargo/memory_plugin 1.6.6 → 1.6.8

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 (48) hide show
  1. package/CHANGELOG.md +148 -101
  2. package/README.md +436 -304
  3. package/mcp-server/cli/direct_commands.js +39 -0
  4. package/mcp-server/cli.js +16 -5
  5. package/mcp-server/cli_boot.js +4 -1
  6. package/mcp-server/client_cli.js +73 -0
  7. package/mcp-server/client_paths.js +44 -0
  8. package/mcp-server/client_registration.js +38 -0
  9. package/mcp-server/codex_config.js +86 -8
  10. package/mcp-server/db/database.js +14 -21
  11. package/mcp-server/db/migrations.js +66 -77
  12. package/mcp-server/db/rag_blob_transport.js +143 -0
  13. package/mcp-server/db/rag_sync.js +284 -0
  14. package/mcp-server/db/sync_queue.js +219 -307
  15. package/mcp-server/dev_link.js +142 -0
  16. package/mcp-server/fact_format.js +44 -12
  17. package/mcp-server/index.js +17 -7
  18. package/mcp-server/ingest/exporter.js +44 -38
  19. package/mcp-server/ingest/normalizer.js +1 -1
  20. package/mcp-server/ingest/pipeline.js +260 -248
  21. package/mcp-server/persona_migration.js +39 -0
  22. package/mcp-server/prompt_manager.js +162 -55
  23. package/mcp-server/retrieval/retriever.js +99 -64
  24. package/mcp-server/setup.js +150 -100
  25. package/mcp-server/storage/blob_store.js +53 -1
  26. package/mcp-server/tools/core/knowledge_read_core.js +163 -0
  27. package/mcp-server/tools/core/memory_core.js +24 -4
  28. package/mcp-server/tools/core/memory_routing.js +10 -0
  29. package/mcp-server/tools/core/note_core.js +53 -0
  30. package/mcp-server/tools/core/rag_query_core.js +169 -0
  31. package/mcp-server/tools/index.js +11 -9
  32. package/mcp-server/tools/memory_tools.js +4 -1
  33. package/mcp-server/tools/note_tools.js +35 -0
  34. package/mcp-server/tools/rag_tools.js +211 -364
  35. package/mcp-server/uninstall.js +627 -0
  36. package/opencode-plugin/index.js +80 -12
  37. package/opencode-plugin/main.js +136 -0
  38. package/package.json +25 -5
  39. package/skills/using-memory/SKILL.md +28 -19
  40. package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
  41. package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
  42. package/mcp-server/benchmarks/policy_dominance_test.js +0 -221
  43. package/mcp-server/benchmarks/quality_evaluator.js +0 -598
  44. package/mcp-server/benchmarks/raw_corpus_data.js +0 -613
  45. package/mcp-server/benchmarks/run_benchmarks.js +0 -366
  46. package/mcp-server/benchmarks/stress_ingestion.js +0 -195
  47. package/mcp-server/benchmarks/table_code_retrieval.js +0 -453
  48. package/mcp-server/benchmarks/test_dual_layer.js +0 -141
@@ -30,6 +30,7 @@ import {
30
30
  factTitle,
31
31
  factBody,
32
32
  autoGenerateTitle,
33
+ isDirectiveFact,
33
34
  } from "../../fact_format.js";
34
35
  import { requireProjectKey, resolveFactIndex } from "../helpers.js";
35
36
 
@@ -71,9 +72,10 @@ async function resolveScopeKey(scope, args = {}, ctx = {}) {
71
72
  }
72
73
 
73
74
  export async function rememberFact(
74
- { fact, title, scope, directory, project, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes },
75
+ { fact, title, kind = "fact", scope, directory, project, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes },
75
76
  ctx = {}
76
77
  ) {
78
+ if (!['fact', 'directive'].includes(kind)) throw new Error("kind must be 'fact' or 'directive'.");
77
79
  const key = requireProjectKey(await resolveScopeKey(scope, { directory, project }, ctx));
78
80
  const entries = await readMemory(key);
79
81
 
@@ -87,7 +89,7 @@ export async function rememberFact(
87
89
  let supersededInfo = "";
88
90
  if (!duplicate) {
89
91
  const [date, time] = today().split(" ");
90
- const meta = { ttl, tags };
92
+ const meta = { ttl, tags, kind };
91
93
  if (keep) meta.keep = "1";
92
94
  if (supersedes) {
93
95
  const targetIdx = resolveFactIndex(entries, supersedes);
@@ -109,6 +111,7 @@ export async function rememberFact(
109
111
  if (!meta.id) meta.id = nextFactId(entries);
110
112
  entries.push(formatFactEntry({ date, time, text, meta }));
111
113
  await writeMemory(key, entries);
114
+ if (key === GLOBAL_KEY && kind === "directive") await syncGlobalPersonaPrompts();
112
115
  }
113
116
 
114
117
  let linkInfo = "";
@@ -189,6 +192,7 @@ export async function recallFacts(
189
192
  if (isKeepFact(factLine)) badges.push("KEEP");
190
193
  if (isSuperseded(factLine)) badges.push("SUPERSEDED");
191
194
  if (meta.inject === "1") badges.push("INJECT");
195
+ if (isDirectiveFact(factLine)) badges.push("DIRECTIVE");
192
196
  if (meta.id) badges.push(`id:${meta.id}`);
193
197
  if (meta.tags) badges.push(`tags:${meta.tags}`);
194
198
  badges.push(`${p.date} ${p.time}`);
@@ -310,11 +314,13 @@ export async function forgetFacts({ query, scope, force, directory, project }, c
310
314
  if (!indices.length) return "Not found.";
311
315
 
312
316
  const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
317
+ const removedGlobalDirective = key === GLOBAL_KEY && removable.some((i) => isDirectiveFact(entries[i]));
313
318
  const protectedCount = indices.length - removable.length;
314
319
  if (removable.length) {
315
320
  const removedBodies = removable.map((i) => factBody(entries[i]) || factText(entries[i]));
316
321
  for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
317
322
  await writeMemory(key, entries);
323
+ if (removedGlobalDirective) await syncGlobalPersonaPrompts();
318
324
  try {
319
325
  const { getDatabase } = await import("../../db/database.js");
320
326
  const { deleteLinksForFacts } = await import("../../graph/knowledge_linker.js");
@@ -326,7 +332,8 @@ export async function forgetFacts({ query, scope, force, directory, project }, c
326
332
  return text;
327
333
  }
328
334
 
329
- export async function updateFactText({ id, newText, title, scope, directory, project }, ctx = {}) {
335
+ export async function updateFactText({ id, newText, title, kind, scope, directory, project }, ctx = {}) {
336
+ if (kind && !['fact', 'directive'].includes(kind)) throw new Error("kind must be 'fact' or 'directive'.");
330
337
  const key = requireProjectKey(await resolveScopeKey(scope, { directory, project }, ctx));
331
338
  const entries = await readMemory(key);
332
339
  const idx = resolveFactIndex(entries, id);
@@ -335,6 +342,7 @@ export async function updateFactText({ id, newText, title, scope, directory, pro
335
342
  const p = parseFactEntry(entries[idx]);
336
343
  const oldText = p ? p.text : entries[idx];
337
344
  const oldBody = factBody(entries[idx]) || oldText;
345
+ const wasDirective = isDirectiveFact(entries[idx]);
338
346
 
339
347
  let { finalTitle, finalFact } = splitTitle(newText, title);
340
348
  if (!finalTitle) finalTitle = factTitle(entries[idx]) || autoGenerateTitle(finalFact);
@@ -343,9 +351,10 @@ export async function updateFactText({ id, newText, title, scope, directory, pro
343
351
  date: p.date,
344
352
  time: p.time,
345
353
  text: `**${finalTitle}** — ${finalFact}`,
346
- meta: p.meta,
354
+ meta: kind ? { ...p.meta, kind } : p.meta,
347
355
  });
348
356
  await writeMemory(key, entries);
357
+ if (key === GLOBAL_KEY && (wasDirective || isDirectiveFact(entries[idx]))) await syncGlobalPersonaPrompts();
349
358
 
350
359
  let linksUpdated = 0;
351
360
  try {
@@ -391,6 +400,17 @@ export async function updateFactText({ id, newText, title, scope, directory, pro
391
400
  return `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}`;
392
401
  }
393
402
 
403
+ async function syncGlobalPersonaPrompts() {
404
+ if (process.env.MEMORY_DISABLE_PERSONA_SYNC === "1") return;
405
+ try {
406
+ const { syncPersonaPrompts } = await import("../../prompt_manager.js");
407
+ await syncPersonaPrompts();
408
+ } catch {
409
+ // Memory persistence must not fail because an optional client config is
410
+ // unavailable or read-only. Explicit sync-persona surfaces such failures.
411
+ }
412
+ }
413
+
394
414
  export async function memoryInfo(_args = {}, ctx = {}) {
395
415
  const effectiveDir = extractEffectiveDir(_args, ctx) || process.cwd();
396
416
  const dbPath = join(MEMORY_DIR, "storage", "memory.sqlite");
@@ -0,0 +1,10 @@
1
+ export const MEMORY_ROUTING_POLICY = `MEMORY ROUTING DIRECTIVE:
2
+ - Use remember for concise durable facts, preferences, constraints, conventions, and decisions that should remain hot and frequently available.
3
+ - Use remember_note for high-value long-form internal memory: decision rationale, investigations, research/experiment results, implementation context, and handoffs that may matter later but should not be auto-injected every session.
4
+ - Use ingest_document for external reusable source material such as files, documentation, codebases, URLs, reports, specifications, and authoritative research sources.
5
+ - Do not save transient conversation noise, routine progress chatter, or disposable intermediate output into either Notebook memory or RAG Memory Notes.
6
+ - Do not duplicate a long note body into remember. When both hot orientation and detailed cold context are useful, save one concise Notebook fact and one detailed RAG Memory Note, then link the fact to the note/document when appropriate.
7
+ - When searching cold memory or knowledge and you first need to identify the right source, prefer query_knowledge_base with resultMode="index", inspect the compact candidates, then expand only the selected doc_id with manage_knowledge_base(action="read_document"). Use resultMode="snippet" when you already want retrieved passage content.`;
8
+
9
+ export const MEMORY_ROUTING_SHORT =
10
+ "remember = concise hot durable fact; remember_note = long-form cold internal memory; ingest_document = external reusable source. Avoid transient noise and duplicate hot/cold bodies.";
@@ -0,0 +1,53 @@
1
+ import { resolveRagScopeKey } from "../../rag_scope.js";
2
+
3
+ /**
4
+ * Shared RAG Memory Note implementation used by MCP and native OpenCode surfaces.
5
+ *
6
+ * The host agent decides what deserves cold/episodic memory. This helper only
7
+ * resolves visibility and routes the note through the existing RAG ingestion
8
+ * pipeline so both tool surfaces keep identical storage semantics.
9
+ */
10
+ export async function rememberNote(
11
+ {
12
+ title,
13
+ content,
14
+ scope = "project",
15
+ kind = "note",
16
+ tags = null,
17
+ directory = null,
18
+ project = null,
19
+ generateEmbeddings = true,
20
+ },
21
+ ctx = {}
22
+ ) {
23
+ const effectiveDirectory = directory || project || ctx.directory || null;
24
+ const projectScope = await resolveRagScopeKey(scope || "project", {
25
+ worktree: ctx.worktree ?? null,
26
+ directory: effectiveDirectory,
27
+ });
28
+
29
+ const { ingestNote } = await import("../../ingest/pipeline.js");
30
+ const result = await ingestNote({
31
+ title,
32
+ content,
33
+ kind,
34
+ tags,
35
+ generateEmbeddings: generateEmbeddings !== false,
36
+ projectScope,
37
+ });
38
+
39
+ return {
40
+ status: "success",
41
+ docId: result.docId,
42
+ blobHash: result.blobHash,
43
+ path: result.path,
44
+ title: result.title,
45
+ sourceType: result.sourceType,
46
+ kind: result.kind,
47
+ tags: result.tags,
48
+ sectionsCount: result.sectionsCount,
49
+ microChunksCount: result.microChunksCount,
50
+ deduplicated: result.deduplicated,
51
+ scope: result.projectScope,
52
+ };
53
+ }
@@ -0,0 +1,169 @@
1
+ import { resolveRagScopeKeys } from "../../rag_scope.js";
2
+ import { getConfig } from "../../config/config_manager.js";
3
+ import { hybridQuery, batchHybridQuery } from "../../retrieval/retriever.js";
4
+
5
+ function normalizeResultMode(resultMode) {
6
+ return resultMode === "index" ? "index" : "snippet";
7
+ }
8
+
9
+ function formatTimestamp(value) {
10
+ if (value === null || value === undefined || value === "") return null;
11
+ const numeric = Number(value);
12
+ const date = Number.isFinite(numeric) ? new Date(numeric) : new Date(value);
13
+ return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
14
+ }
15
+
16
+ async function ensureRagFresh() {
17
+ if (getConfig().mode !== "hybrid-sync") return;
18
+ const { ensureReverseSync } = await import("../../db/sync_queue.js");
19
+ await ensureReverseSync();
20
+ }
21
+
22
+ function formatIdentityLines(result) {
23
+ const lines = [];
24
+ if (result.doc_id) lines.push(`Doc ID: ${result.doc_id}`);
25
+ if (result.source_type) lines.push(`Source: ${result.source_type}`);
26
+ if (result.note_kind) lines.push(`Kind: ${result.note_kind}`);
27
+ if (Array.isArray(result.tags) && result.tags.length > 0) {
28
+ lines.push(`Tags: ${result.tags.join(", ")}`);
29
+ }
30
+ return lines;
31
+ }
32
+
33
+ export function formatSnippetResult(result, rank, headingLevel = 3) {
34
+ const hashes = "#".repeat(Math.max(1, headingLevel));
35
+ let header = `${hashes} [${rank}] ${result.doc_title || "Untitled"}`;
36
+ if (result.heading) header += ` > ${result.heading}`;
37
+ if (result.breadcrumbs) header += ` (${result.breadcrumbs})`;
38
+
39
+ const bodyLines = [
40
+ ...formatIdentityLines(result),
41
+ `Score: ${(result.score || 0).toFixed(4)}`,
42
+ ];
43
+
44
+ if (result.defined_symbols && result.defined_symbols.length > 0) {
45
+ bodyLines.push(`Defined Symbols: ${result.defined_symbols.join(", ")}`);
46
+ }
47
+
48
+ const body = result.snippet || result.full_section_content || "";
49
+ return `${header}\n${bodyLines.join("\n")}\n\n${body}`;
50
+ }
51
+
52
+ export function formatIndexResult(result, rank, headingLevel = 3) {
53
+ const hashes = "#".repeat(Math.max(1, headingLevel));
54
+ let header = `${hashes} [${rank}] ${result.doc_title || "Untitled"}`;
55
+ if (result.heading) header += ` > ${result.heading}`;
56
+ if (result.breadcrumbs) header += ` (${result.breadcrumbs})`;
57
+
58
+ const lines = [
59
+ ...formatIdentityLines(result),
60
+ `Score: ${(result.score || 0).toFixed(4)}`,
61
+ ];
62
+
63
+ if (result.retrieval_policy) lines.push(`Policy: ${result.retrieval_policy}`);
64
+ const createdAt = formatTimestamp(result.doc_created_at);
65
+ const updatedAt = formatTimestamp(result.doc_updated_at);
66
+ if (createdAt) lines.push(`Created: ${createdAt}`);
67
+ if (updatedAt) lines.push(`Updated: ${updatedAt}`);
68
+
69
+ return `${header}\n${lines.join("\n")}`;
70
+ }
71
+
72
+ function formatResult(result, rank, resultMode, headingLevel) {
73
+ return resultMode === "index"
74
+ ? formatIndexResult(result, rank, headingLevel)
75
+ : formatSnippetResult(result, rank, headingLevel);
76
+ }
77
+
78
+ export async function runSingleRagQuery(
79
+ {
80
+ query,
81
+ limit = 5,
82
+ instruction = null,
83
+ generateEmbeddings = true,
84
+ scope = "all",
85
+ directory = null,
86
+ project = null,
87
+ resultMode = "snippet",
88
+ },
89
+ ctx = {}
90
+ ) {
91
+ const mode = normalizeResultMode(resultMode);
92
+ const activeConfig = getConfig();
93
+ await ensureRagFresh();
94
+
95
+ const effectiveDirectory = directory || project || ctx.directory || null;
96
+ const scopeKeys = await resolveRagScopeKeys(scope || "all", {
97
+ worktree: ctx.worktree ?? null,
98
+ directory: effectiveDirectory,
99
+ });
100
+
101
+ const results = await hybridQuery({
102
+ query,
103
+ limit,
104
+ generateEmbeddings: generateEmbeddings !== false,
105
+ instruction: instruction || null,
106
+ scopeKeys,
107
+ includeGraphContext: mode !== "index",
108
+ policyExpansion: mode !== "index",
109
+ });
110
+
111
+ if (!results || results.length === 0) {
112
+ return `[Active Model: ${activeConfig.embeddingModel} | Mode: ${mode}]\nNo matching knowledge found for query.`;
113
+ }
114
+
115
+ const header = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()} | Mode: ${mode}]\n\n`;
116
+ const formatted = results
117
+ .map((result, index) => formatResult(result, index + 1, mode, 3))
118
+ .join("\n\n---\n\n");
119
+
120
+ return header + formatted;
121
+ }
122
+
123
+ export async function runBatchRagQuery(
124
+ {
125
+ queries,
126
+ limit = 5,
127
+ instruction = null,
128
+ generateEmbeddings = true,
129
+ scope = "all",
130
+ directory = null,
131
+ project = null,
132
+ resultMode = "snippet",
133
+ },
134
+ ctx = {}
135
+ ) {
136
+ const mode = normalizeResultMode(resultMode);
137
+ const activeConfig = getConfig();
138
+ await ensureRagFresh();
139
+
140
+ const effectiveDirectory = directory || project || ctx.directory || null;
141
+ const scopeKeys = await resolveRagScopeKeys(scope || "all", {
142
+ worktree: ctx.worktree ?? null,
143
+ directory: effectiveDirectory,
144
+ });
145
+
146
+ const allResults = await batchHybridQuery(queries, {
147
+ limit,
148
+ generateEmbeddings: generateEmbeddings !== false,
149
+ instruction: instruction || null,
150
+ scopeKeys,
151
+ includeGraphContext: mode !== "index",
152
+ policyExpansion: mode !== "index",
153
+ });
154
+
155
+ const formatted = allResults
156
+ .map((results, queryIndex) => {
157
+ const queryHeader = `### Query [${queryIndex + 1}]: "${queries[queryIndex]}"\n\n`;
158
+ if (!results || results.length === 0) {
159
+ return queryHeader + "No matching knowledge found for this query.";
160
+ }
161
+ const items = results
162
+ .map((result, resultIndex) => formatResult(result, resultIndex + 1, mode, 4))
163
+ .join("\n\n---\n\n");
164
+ return queryHeader + items;
165
+ })
166
+ .join("\n\n===\n\n");
167
+
168
+ return `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()} | Mode: ${mode} | ${queries.length} queries]\n\n${formatted}`;
169
+ }
@@ -1,9 +1,11 @@
1
- import { registerMemoryTools } from "./memory_tools.js";
2
- import { registerIdentityTools } from "./identity_tools.js";
3
- import { registerRagTools } from "./rag_tools.js";
4
-
5
- export function registerAllTools(server) {
6
- registerMemoryTools(server);
7
- registerIdentityTools(server);
8
- registerRagTools(server);
9
- }
1
+ import { registerMemoryTools } from "./memory_tools.js";
2
+ import { registerIdentityTools } from "./identity_tools.js";
3
+ import { registerRagTools } from "./rag_tools.js";
4
+ import { registerNoteTools } from "./note_tools.js";
5
+
6
+ export function registerAllTools(server) {
7
+ registerMemoryTools(server);
8
+ registerIdentityTools(server);
9
+ registerRagTools(server);
10
+ registerNoteTools(server);
11
+ }
@@ -16,6 +16,7 @@ export function registerMemoryTools(server) {
16
16
  description:
17
17
  "Save an important, durable fact to memory. Only use for high-signal information " +
18
18
  "(name, goals, constraints, tech preferences, project conventions). " +
19
+ "Set kind='directive' only for active user-approved personality, behavior, tone, style, preference, or working instructions; use kind='fact' for descriptive context. " +
19
20
  "directory: optional workspace/project directory path to target (ensures saving into the project store even from external cwd). " +
20
21
  "docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
21
22
  "Knowledge Base document or line range; omit them when no linking is needed. " +
@@ -29,6 +30,7 @@ export function registerMemoryTools(server) {
29
30
  inputSchema: z.object({
30
31
  fact: z.string().describe("The fact to remember, written in English"),
31
32
  title: optStr().describe("Optional title for the fact. If not specified, one is auto-generated."),
33
+ kind: z.enum(["fact", "directive"]).nullish().transform((v) => v || "fact").describe("'fact' (context) or 'directive' (active personalization/working instruction)"),
32
34
  scope: defStr("project").describe("'project' (default) or 'global'"),
33
35
  directory: optStr().describe("Optional workspace/project directory path to target when scope='project' (e.g. 'F:/projects/my-app')"),
34
36
  project: optStr().describe("Alias for directory"),
@@ -109,11 +111,12 @@ export function registerMemoryTools(server) {
109
111
  {
110
112
  description:
111
113
  "Update the text of an existing fact by number (from recall), id, or text match, " +
112
- "preserving its original date and metadata. Linked Knowledge Base documents are re-pointed to the new text.",
114
+ "preserving its original date and metadata. kind can optionally reclassify it as context ('fact') or active personalization ('directive'). Linked Knowledge Base documents are re-pointed to the new text.",
113
115
  inputSchema: z.object({
114
116
  id: z.string().describe("Number (from recall), metadata id, or text of the fact to update"),
115
117
  newText: z.string().describe("New fact text"),
116
118
  title: optStr().describe("Optional new title for the fact"),
119
+ kind: z.enum(["fact", "directive"]).nullish().describe("Optional new semantic kind: 'fact' or 'directive'"),
117
120
  scope: defStr("project").describe("'project' (default) or 'global'"),
118
121
  directory: optStr().describe("Optional workspace/project directory path"),
119
122
  project: optStr().describe("Alias for directory"),
@@ -0,0 +1,35 @@
1
+ import * as z from "zod/v4";
2
+ import { optStr, defBool } from "./helpers.js";
3
+ import { rememberNote } from "./core/note_core.js";
4
+
5
+ const NOTE_KINDS = ["decision", "research", "context", "handoff", "note"];
6
+
7
+ export function registerNoteTools(server) {
8
+ server.registerTool(
9
+ "remember_note",
10
+ {
11
+ description:
12
+ "Save high-value long-form or episodic context as a cold RAG Memory Note. " +
13
+ "Use this for decisions with rationale, research/experiment results, investigations, handoffs, and detailed context that may matter later but should NOT be injected into every session. " +
14
+ "Use remember() instead for concise durable facts that should stay hot/automatically available. " +
15
+ "Use ingest_document() instead for external reusable truth sources such as files, URLs, documentation, reports, or codebases. " +
16
+ "The note is indexed in the existing RAG knowledge base and can later be found semantically and expanded by document ID.",
17
+ inputSchema: z.object({
18
+ title: z.string().trim().min(1).describe("Concise descriptive title for the memory note"),
19
+ content: z.string().refine((value) => value.trim().length > 0, "RAG Memory Note content must not be empty").describe("Full long-form note content to preserve"),
20
+ scope: z.enum(["project", "global"]).nullish().transform((v) => v || "project").describe("Visibility: current Git project (default) or global"),
21
+ kind: z.enum(NOTE_KINDS).catch("note").nullish().transform((v) => v || "note").describe("Note kind: decision, research, context, handoff, or note"),
22
+ tags: optStr().describe("Optional comma-separated tags; normalized to lowercase unique values"),
23
+ directory: optStr().describe("Optional workspace/project directory path to target"),
24
+ project: optStr().describe("Alias for directory"),
25
+ generateEmbeddings: defBool(true).describe("Compute dense vector embeddings; set false for offline/tests"),
26
+ }),
27
+ },
28
+ async (args) => {
29
+ const result = await rememberNote(args);
30
+ return {
31
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
32
+ };
33
+ }
34
+ );
35
+ }