getmnemo-vercel-ai 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mnemo, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @ledgermem/vercel-ai
2
+
3
+ LedgerMem adapter for the [Vercel AI SDK](https://sdk.vercel.dev). Drop-in
4
+ `tool()` definitions that let any model search and write persistent memory,
5
+ plus a small React hook for client-side memory views.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @ledgermem/vercel-ai @ledgermem/memory ai
11
+ ```
12
+
13
+ Set `LEDGERMEM_API_KEY` and `LEDGERMEM_WORKSPACE_ID` in your environment, or
14
+ pass them explicitly.
15
+
16
+ ## Quickstart (30 seconds)
17
+
18
+ ```ts
19
+ import { streamText } from "ai";
20
+ import { openai } from "@ai-sdk/openai";
21
+ import { ledgermemTools } from "@ledgermem/vercel-ai";
22
+
23
+ const result = await streamText({
24
+ model: openai("gpt-4o"),
25
+ tools: ledgermemTools, // memorySearch + memoryAdd
26
+ maxSteps: 5,
27
+ messages: [{ role: "user", content: "What did I tell you about my coffee?" }],
28
+ });
29
+
30
+ for await (const chunk of result.textStream) process.stdout.write(chunk);
31
+ ```
32
+
33
+ ## Per-user memory (route handler)
34
+
35
+ ```ts
36
+ import { createLedgerMemTools } from "@ledgermem/vercel-ai";
37
+
38
+ export async function POST(req: Request) {
39
+ const { messages, userId } = await req.json();
40
+ const tools = createLedgerMemTools({ metadata: { userId } });
41
+ return streamText({ model: openai("gpt-4o"), tools, messages }).toDataStreamResponse();
42
+ }
43
+ ```
44
+
45
+ ## React hook
46
+
47
+ ```tsx
48
+ "use client";
49
+ import { useLedgerMem } from "@ledgermem/vercel-ai/react";
50
+
51
+ export function MemorySidebar() {
52
+ const { results, search, loading } = useLedgerMem({ initialQuery: "preferences" });
53
+ return (
54
+ <ul>
55
+ {results.map((m: any) => <li key={m.id}>{m.content}</li>)}
56
+ </ul>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,2 @@
1
+ export { createMnemoTools, getmnemoTools, type MnemoToolsOptions, type MnemoToolset, } from "./tools.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,KAAK,iBAAiB,EACtB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createMnemoTools, getmnemoTools, } from "./tools.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,aAAa,GAGd,MAAM,YAAY,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { Mnemo } from "getmnemo";
2
+ export interface UseMnemoOptions {
3
+ apiKey?: string;
4
+ workspaceId?: string;
5
+ client?: Mnemo;
6
+ /** Initial query to run on mount; pass `undefined` to skip. */
7
+ initialQuery?: string;
8
+ initialLimit?: number;
9
+ }
10
+ export interface UseMnemoResult<T = unknown> {
11
+ results: T[];
12
+ loading: boolean;
13
+ error: Error | null;
14
+ search: (query: string, limit?: number) => Promise<T[]>;
15
+ add: (content: string, metadata?: Record<string, unknown>) => Promise<T>;
16
+ remove: (id: string) => Promise<void>;
17
+ }
18
+ /**
19
+ * Tiny React hook for direct Mnemo access from client components.
20
+ *
21
+ * For tool-use inside `useChat`, prefer wiring `getmnemoTools` into the
22
+ * server route instead — this hook is for sidebars / memory inspectors.
23
+ */
24
+ export declare function useMnemo<T = unknown>(options?: UseMnemoOptions): UseMnemoResult<T>;
25
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IACzC,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACxD,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAClC,OAAO,GAAE,eAAoB,GAC5B,cAAc,CAAC,CAAC,CAAC,CAsFnB"}
package/dist/react.js ADDED
@@ -0,0 +1,92 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { Mnemo } from "getmnemo";
3
+ /**
4
+ * Tiny React hook for direct Mnemo access from client components.
5
+ *
6
+ * For tool-use inside `useChat`, prefer wiring `getmnemoTools` into the
7
+ * server route instead — this hook is for sidebars / memory inspectors.
8
+ */
9
+ export function useMnemo(options = {}) {
10
+ const [client] = useState(() => resolveClient(options));
11
+ const [results, setResults] = useState([]);
12
+ const [loading, setLoading] = useState(false);
13
+ const [error, setError] = useState(null);
14
+ // Last-wins guard: every search() bumps the counter and only the most
15
+ // recent call is allowed to write to state. Without this, two rapid
16
+ // searches let the slower one overwrite the fresher one (the classic
17
+ // "stale request wins" race), and unmounting mid-flight wrote state on
18
+ // a torn-down component.
19
+ const requestIdRef = useRef(0);
20
+ const mountedRef = useRef(true);
21
+ useEffect(() => {
22
+ mountedRef.current = true;
23
+ return () => {
24
+ mountedRef.current = false;
25
+ };
26
+ }, []);
27
+ const search = useCallback(async (query, limit = 5) => {
28
+ const myId = ++requestIdRef.current;
29
+ setLoading(true);
30
+ setError(null);
31
+ try {
32
+ const r = ((await client.search({ query, limit })).hits);
33
+ if (mountedRef.current && myId === requestIdRef.current) {
34
+ setResults(r);
35
+ }
36
+ return r;
37
+ }
38
+ catch (e) {
39
+ const err = e instanceof Error ? e : new Error(String(e));
40
+ if (mountedRef.current && myId === requestIdRef.current) {
41
+ setError(err);
42
+ }
43
+ throw err;
44
+ }
45
+ finally {
46
+ if (mountedRef.current && myId === requestIdRef.current) {
47
+ setLoading(false);
48
+ }
49
+ }
50
+ }, [client]);
51
+ const add = useCallback(async (content, metadata = {}) => {
52
+ // Bump the request id BEFORE awaiting so any in-flight ``search``
53
+ // started earlier loses the last-wins race and cannot wipe the
54
+ // memory we are about to prepend. Without this, a fast
55
+ // ``add()`` immediately after a slow ``search()`` would see the
56
+ // search resolve last and clobber the freshly-added entry.
57
+ const myId = ++requestIdRef.current;
58
+ const memory = (await client.add({ content, metadata }));
59
+ if (mountedRef.current && myId === requestIdRef.current) {
60
+ setResults((prev) => [memory, ...prev]);
61
+ }
62
+ return memory;
63
+ }, [client]);
64
+ const remove = useCallback(async (id) => {
65
+ const myId = ++requestIdRef.current;
66
+ await client.delete(id);
67
+ // Same last-wins guard as ``add`` — a slow concurrent ``search``
68
+ // could otherwise resolve after the delete and re-introduce the
69
+ // just-removed row into the rendered list.
70
+ if (mountedRef.current && myId === requestIdRef.current) {
71
+ setResults((prev) => prev.filter((r) => r?.id !== id));
72
+ }
73
+ }, [client]);
74
+ useEffect(() => {
75
+ if (options.initialQuery) {
76
+ void search(options.initialQuery, options.initialLimit);
77
+ }
78
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
+ }, []);
80
+ return { results, loading, error, search, add, remove };
81
+ }
82
+ function resolveClient(opts) {
83
+ if (opts.client)
84
+ return opts.client;
85
+ const apiKey = opts.apiKey ?? process.env.NEXT_PUBLIC_GETMNEMO_API_KEY;
86
+ const workspaceId = opts.workspaceId ?? process.env.NEXT_PUBLIC_GETMNEMO_WORKSPACE_ID;
87
+ if (!apiKey || !workspaceId) {
88
+ throw new Error("useMnemo: missing apiKey/workspaceId. Note: client-side keys are public — prefer a server route in production.");
89
+ }
90
+ return new Mnemo({ apiKey, workspaceId });
91
+ }
92
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAoBjC;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CACtB,UAA2B,EAAE;IAE7B,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAM,EAAE,CAAC,CAAC;IAChD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IACvD,sEAAsE;IACtE,oEAAoE;IACpE,qEAAqE;IACrE,uEAAuE;IACvE,yBAAyB;IACzB,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAC/B,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,CAAC,OAAO,GAAG,IAAI,CAAC;QAC1B,OAAO,GAAG,EAAE;YACV,UAAU,CAAC,OAAO,GAAG,KAAK,CAAC;QAC7B,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,MAAM,GAAG,WAAW,CACxB,KAAK,EAAE,KAAa,EAAE,KAAK,GAAG,CAAC,EAAgB,EAAE;QAC/C,MAAM,IAAI,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC;QACpC,UAAU,CAAC,IAAI,CAAC,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAmB,CAAC;YAC3E,IAAI,UAAU,CAAC,OAAO,IAAI,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC;gBACxD,UAAU,CAAC,CAAC,CAAC,CAAC;YAChB,CAAC;YACD,OAAO,CAAC,CAAC;QACX,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1D,IAAI,UAAU,CAAC,OAAO,IAAI,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC;gBACxD,QAAQ,CAAC,GAAG,CAAC,CAAC;YAChB,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,IAAI,UAAU,CAAC,OAAO,IAAI,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC;gBACxD,UAAU,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,GAAG,GAAG,WAAW,CACrB,KAAK,EAAE,OAAe,EAAE,WAAoC,EAAE,EAAE,EAAE;QAChE,kEAAkE;QAClE,+DAA+D;QAC/D,uDAAuD;QACvD,gEAAgE;QAChE,2DAA2D;QAC3D,MAAM,IAAI,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC;QACpC,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAM,CAAC;QAC9D,IAAI,UAAU,CAAC,OAAO,IAAI,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC;YACxD,UAAU,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,MAAM,GAAG,WAAW,CACxB,KAAK,EAAE,EAAU,EAAE,EAAE;QACnB,MAAM,IAAI,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC;QACpC,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACxB,iEAAiE;QACjE,gEAAgE;QAChE,2CAA2C;QAC3C,IAAI,UAAU,CAAC,OAAO,IAAI,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC;YACxD,UAAU,CAAC,CAAC,IAAI,EAAE,EAAE,CAClB,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAAqB,EAAE,EAAE,KAAK,EAAE,CAAC,CACtD,CAAC;QACJ,CAAC;IACH,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;YACzB,KAAK,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;QAC1D,CAAC;QACD,uDAAuD;IACzD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;AAC1D,CAAC;AAED,SAAS,aAAa,CAAC,IAAqB;IAC1C,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC,MAAM,CAAC;IACpC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC;IACvE,MAAM,WAAW,GACf,IAAI,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC;IACpE,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,gHAAgH,CACjH,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { Mnemo } from "getmnemo";
2
+ import { tool } from "ai";
3
+ /**
4
+ * Options for constructing a Mnemo toolset.
5
+ *
6
+ * Either pass `client` (a pre-built `Mnemo` instance) or `apiKey` +
7
+ * `workspaceId` and one will be created for you.
8
+ */
9
+ export interface MnemoToolsOptions {
10
+ client?: Mnemo;
11
+ apiKey?: string;
12
+ workspaceId?: string;
13
+ /** Default `limit` passed to `search` when the model omits it. */
14
+ defaultLimit?: number;
15
+ /** Static metadata merged into every `add` call (e.g. `{ userId }`). */
16
+ metadata?: Record<string, unknown>;
17
+ }
18
+ export interface MnemoToolset {
19
+ memorySearch: ReturnType<typeof tool>;
20
+ memoryAdd: ReturnType<typeof tool>;
21
+ }
22
+ /**
23
+ * Build a pair of Vercel AI SDK tools backed by Mnemo.
24
+ *
25
+ * Drop the returned object into `streamText({ tools })` or
26
+ * `generateText({ tools })` and the model can search and write
27
+ * persistent memory.
28
+ */
29
+ export declare function createMnemoTools(options?: MnemoToolsOptions): MnemoToolset;
30
+ /**
31
+ * Pre-built default toolset using `GETMNEMO_API_KEY` and
32
+ * `GETMNEMO_WORKSPACE_ID` from `process.env`.
33
+ *
34
+ * Lazy — the client isn't constructed until a tool actually runs.
35
+ */
36
+ export declare const getmnemoTools: MnemoToolset;
37
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAG1B;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;IACtC,SAAS,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;CACpC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,iBAAsB,GAC9B,YAAY,CA8Dd;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,YAMxB,CAAC"}
package/dist/tools.js ADDED
@@ -0,0 +1,90 @@
1
+ import { Mnemo } from "getmnemo";
2
+ import { tool } from "ai";
3
+ import { z } from "zod";
4
+ /**
5
+ * Build a pair of Vercel AI SDK tools backed by Mnemo.
6
+ *
7
+ * Drop the returned object into `streamText({ tools })` or
8
+ * `generateText({ tools })` and the model can search and write
9
+ * persistent memory.
10
+ */
11
+ export function createMnemoTools(options = {}) {
12
+ const client = resolveClient(options);
13
+ const defaultLimit = options.defaultLimit ?? 5;
14
+ const baseMetadata = options.metadata ?? {};
15
+ const memorySearch = tool({
16
+ description: "Search the user's long-term memory for facts, preferences, or past conversations relevant to the current query. Returns the most relevant snippets.",
17
+ // .strict() emits additionalProperties:false so providers that honour
18
+ // strict JSON schema (OpenAI, Anthropic) reject hallucinated keys
19
+ // instead of silently dropping them at parse time.
20
+ parameters: z
21
+ .object({
22
+ query: z
23
+ .string()
24
+ .min(1)
25
+ .describe("Natural-language query describing what to recall."),
26
+ limit: z
27
+ .number()
28
+ .int()
29
+ .positive()
30
+ .max(50)
31
+ .optional()
32
+ .describe("Max number of memories to return."),
33
+ }),
34
+ execute: async ({ query, limit }) => {
35
+ // Clamp the limit defensively — the schema constrains the model,
36
+ // but a non-conforming provider response could still smuggle a
37
+ // huge value through and blow the context window.
38
+ const requested = limit ?? defaultLimit;
39
+ const safeLimit = Math.min(50, Math.max(1, Math.floor(requested)));
40
+ const results = await client.search({ query, limit: safeLimit });
41
+ return { results };
42
+ },
43
+ });
44
+ const memoryAdd = tool({
45
+ description: "Save a new fact, preference, or noteworthy detail about the user to long-term memory. Use sparingly — only for information worth remembering across sessions.",
46
+ parameters: z
47
+ .object({
48
+ content: z
49
+ .string()
50
+ .min(1)
51
+ .describe("The fact or note to remember, written in plain text."),
52
+ metadata: z
53
+ .record(z.unknown())
54
+ .optional()
55
+ .describe("Optional structured tags (e.g. { topic, source })."),
56
+ }),
57
+ execute: async ({ content, metadata }) => {
58
+ // Model-supplied metadata is merged FIRST so trusted baseMetadata
59
+ // (e.g. userId, workspaceId) cannot be overwritten by prompt injection.
60
+ const merged = { ...(metadata ?? {}), ...baseMetadata };
61
+ const memory = await client.add({ content, metadata: merged });
62
+ return { memory };
63
+ },
64
+ });
65
+ return { memorySearch, memoryAdd };
66
+ }
67
+ /**
68
+ * Pre-built default toolset using `GETMNEMO_API_KEY` and
69
+ * `GETMNEMO_WORKSPACE_ID` from `process.env`.
70
+ *
71
+ * Lazy — the client isn't constructed until a tool actually runs.
72
+ */
73
+ export const getmnemoTools = (() => {
74
+ let cached = null;
75
+ const get = () => (cached ??= createMnemoTools());
76
+ return new Proxy({}, {
77
+ get: (_target, prop) => get()[prop],
78
+ });
79
+ })();
80
+ function resolveClient(opts) {
81
+ if (opts.client)
82
+ return opts.client;
83
+ const apiKey = opts.apiKey ?? process.env.GETMNEMO_API_KEY;
84
+ const workspaceId = opts.workspaceId ?? process.env.GETMNEMO_WORKSPACE_ID;
85
+ if (!apiKey || !workspaceId) {
86
+ throw new Error("createMnemoTools: missing apiKey/workspaceId. Pass them explicitly or set GETMNEMO_API_KEY and GETMNEMO_WORKSPACE_ID.");
87
+ }
88
+ return new Mnemo({ apiKey, workspaceId });
89
+ }
90
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBxB;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAA6B,EAAE;IAE/B,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC;IAC/C,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;IAE5C,MAAM,YAAY,GAAQ,IAAI,CAAC;QAC7B,WAAW,EACT,qJAAqJ;QACvJ,sEAAsE;QACtE,kEAAkE;QAClE,mDAAmD;QACnD,UAAU,EAAE,CAAC;aACV,MAAM,CAAC;YACN,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,mDAAmD,CAAC;YAChE,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,QAAQ,EAAE;iBACV,GAAG,CAAC,EAAE,CAAC;iBACP,QAAQ,EAAE;iBACV,QAAQ,CAAC,mCAAmC,CAAC;SACjD,CAAC;QAEJ,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;YAClC,iEAAiE;YACjE,+DAA+D;YAC/D,kDAAkD;YAClD,MAAM,SAAS,GAAG,KAAK,IAAI,YAAY,CAAC;YACxC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACnE,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YACjE,OAAO,EAAE,OAAO,EAAE,CAAC;QACrB,CAAC;KACF,CAAC,CAAC;IAEH,MAAM,SAAS,GAAQ,IAAI,CAAC;QAC1B,WAAW,EACT,+JAA+J;QACjK,UAAU,EAAE,CAAC;aACV,MAAM,CAAC;YACN,OAAO,EAAE,CAAC;iBACP,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,sDAAsD,CAAC;YACnE,QAAQ,EAAE,CAAC;iBACR,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;iBACnB,QAAQ,EAAE;iBACV,QAAQ,CAAC,oDAAoD,CAAC;SAClE,CAAC;QAEJ,OAAO,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE;YACvC,kEAAkE;YAClE,wEAAwE;YACxE,MAAM,MAAM,GAAG,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,GAAG,YAAY,EAAE,CAAC;YACxD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/D,OAAO,EAAE,MAAM,EAAE,CAAC;QACpB,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;AACrC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAiB,CAAC,GAAG,EAAE;IAC/C,IAAI,MAAM,GAAwB,IAAI,CAAC;IACvC,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC,CAAC,MAAM,KAAK,gBAAgB,EAAE,CAAC,CAAC;IAClD,OAAO,IAAI,KAAK,CAAC,EAAkB,EAAE;QACnC,GAAG,EAAE,CAAC,OAAO,EAAE,IAAY,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,IAA0B,CAAC;KAClE,CAAC,CAAC;AACL,CAAC,CAAC,EAAE,CAAC;AAEL,SAAS,aAAa,CAAC,IAAuB;IAC5C,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC,MAAM,CAAC;IACpC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC3D,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;IAC1E,IAAI,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,uHAAuH,CACxH,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;AAC5C,CAAC"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "getmnemo-vercel-ai",
3
+ "version": "0.1.0",
4
+ "description": "Mnemo adapter for the Vercel AI SDK — drop-in tools for streamText / generateText / useChat.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./react": {
14
+ "types": "./dist/react.d.ts",
15
+ "import": "./dist/react.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "test": "vitest run",
27
+ "lint": "tsc --noEmit"
28
+ },
29
+ "keywords": [
30
+ "getmnemo",
31
+ "vercel-ai",
32
+ "ai-sdk",
33
+ "memory",
34
+ "rag",
35
+ "tools"
36
+ ],
37
+ "author": "Mnemo <founders@getmnemo.xyz>",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/ledgermem/getmnemo-vercel-ai.git"
42
+ },
43
+ "dependencies": {
44
+ "getmnemo": "^0.1.0",
45
+ "zod": "^3.23.0"
46
+ },
47
+ "peerDependencies": {
48
+ "@ai-sdk/react": "^1.0.0",
49
+ "ai": "^4.0.0",
50
+ "react": "^18.0.0 || ^19.0.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "@ai-sdk/react": {
54
+ "optional": true
55
+ },
56
+ "react": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^22.0.0",
62
+ "@types/react": "^19.2.14",
63
+ "ai": "^4.0.0",
64
+ "typescript": "^5.6.0",
65
+ "vitest": "^2.1.0"
66
+ },
67
+ "engines": {
68
+ "node": ">=18.17.0"
69
+ },
70
+ "homepage": "https://getmnemo.xyz"
71
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export {
2
+ createMnemoTools,
3
+ getmnemoTools,
4
+ type MnemoToolsOptions,
5
+ type MnemoToolset,
6
+ } from "./tools.js";
package/src/react.ts ADDED
@@ -0,0 +1,129 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { Mnemo } from "getmnemo";
3
+
4
+ export interface UseMnemoOptions {
5
+ apiKey?: string;
6
+ workspaceId?: string;
7
+ client?: Mnemo;
8
+ /** Initial query to run on mount; pass `undefined` to skip. */
9
+ initialQuery?: string;
10
+ initialLimit?: number;
11
+ }
12
+
13
+ export interface UseMnemoResult<T = unknown> {
14
+ results: T[];
15
+ loading: boolean;
16
+ error: Error | null;
17
+ search: (query: string, limit?: number) => Promise<T[]>;
18
+ add: (content: string, metadata?: Record<string, unknown>) => Promise<T>;
19
+ remove: (id: string) => Promise<void>;
20
+ }
21
+
22
+ /**
23
+ * Tiny React hook for direct Mnemo access from client components.
24
+ *
25
+ * For tool-use inside `useChat`, prefer wiring `getmnemoTools` into the
26
+ * server route instead — this hook is for sidebars / memory inspectors.
27
+ */
28
+ export function useMnemo<T = unknown>(
29
+ options: UseMnemoOptions = {},
30
+ ): UseMnemoResult<T> {
31
+ const [client] = useState(() => resolveClient(options));
32
+ const [results, setResults] = useState<T[]>([]);
33
+ const [loading, setLoading] = useState(false);
34
+ const [error, setError] = useState<Error | null>(null);
35
+ // Last-wins guard: every search() bumps the counter and only the most
36
+ // recent call is allowed to write to state. Without this, two rapid
37
+ // searches let the slower one overwrite the fresher one (the classic
38
+ // "stale request wins" race), and unmounting mid-flight wrote state on
39
+ // a torn-down component.
40
+ const requestIdRef = useRef(0);
41
+ const mountedRef = useRef(true);
42
+ useEffect(() => {
43
+ mountedRef.current = true;
44
+ return () => {
45
+ mountedRef.current = false;
46
+ };
47
+ }, []);
48
+
49
+ const search = useCallback(
50
+ async (query: string, limit = 5): Promise<T[]> => {
51
+ const myId = ++requestIdRef.current;
52
+ setLoading(true);
53
+ setError(null);
54
+ try {
55
+ const r = ((await client.search({ query, limit })).hits) as unknown as T[];
56
+ if (mountedRef.current && myId === requestIdRef.current) {
57
+ setResults(r);
58
+ }
59
+ return r;
60
+ } catch (e) {
61
+ const err = e instanceof Error ? e : new Error(String(e));
62
+ if (mountedRef.current && myId === requestIdRef.current) {
63
+ setError(err);
64
+ }
65
+ throw err;
66
+ } finally {
67
+ if (mountedRef.current && myId === requestIdRef.current) {
68
+ setLoading(false);
69
+ }
70
+ }
71
+ },
72
+ [client],
73
+ );
74
+
75
+ const add = useCallback(
76
+ async (content: string, metadata: Record<string, unknown> = {}) => {
77
+ // Bump the request id BEFORE awaiting so any in-flight ``search``
78
+ // started earlier loses the last-wins race and cannot wipe the
79
+ // memory we are about to prepend. Without this, a fast
80
+ // ``add()`` immediately after a slow ``search()`` would see the
81
+ // search resolve last and clobber the freshly-added entry.
82
+ const myId = ++requestIdRef.current;
83
+ const memory = (await client.add({ content, metadata })) as T;
84
+ if (mountedRef.current && myId === requestIdRef.current) {
85
+ setResults((prev) => [memory, ...prev]);
86
+ }
87
+ return memory;
88
+ },
89
+ [client],
90
+ );
91
+
92
+ const remove = useCallback(
93
+ async (id: string) => {
94
+ const myId = ++requestIdRef.current;
95
+ await client.delete(id);
96
+ // Same last-wins guard as ``add`` — a slow concurrent ``search``
97
+ // could otherwise resolve after the delete and re-introduce the
98
+ // just-removed row into the rendered list.
99
+ if (mountedRef.current && myId === requestIdRef.current) {
100
+ setResults((prev) =>
101
+ prev.filter((r) => (r as { id?: string })?.id !== id),
102
+ );
103
+ }
104
+ },
105
+ [client],
106
+ );
107
+
108
+ useEffect(() => {
109
+ if (options.initialQuery) {
110
+ void search(options.initialQuery, options.initialLimit);
111
+ }
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ }, []);
114
+
115
+ return { results, loading, error, search, add, remove };
116
+ }
117
+
118
+ function resolveClient(opts: UseMnemoOptions): Mnemo {
119
+ if (opts.client) return opts.client;
120
+ const apiKey = opts.apiKey ?? process.env.NEXT_PUBLIC_GETMNEMO_API_KEY;
121
+ const workspaceId =
122
+ opts.workspaceId ?? process.env.NEXT_PUBLIC_GETMNEMO_WORKSPACE_ID;
123
+ if (!apiKey || !workspaceId) {
124
+ throw new Error(
125
+ "useMnemo: missing apiKey/workspaceId. Note: client-side keys are public — prefer a server route in production.",
126
+ );
127
+ }
128
+ return new Mnemo({ apiKey, workspaceId });
129
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import { createMnemoTools } from "./tools.js";
3
+
4
+ vi.mock("getmnemo", () => {
5
+ return {
6
+ Mnemo: vi.fn().mockImplementation(() => ({
7
+ search: vi
8
+ .fn()
9
+ .mockResolvedValue([{ id: "m1", content: "user likes oat milk" }]),
10
+ add: vi.fn().mockResolvedValue({ id: "m2", content: "stored" }),
11
+ update: vi.fn(),
12
+ delete: vi.fn(),
13
+ list: vi.fn(),
14
+ })),
15
+ };
16
+ });
17
+
18
+ describe("createMnemoTools", () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ it("builds memorySearch and memoryAdd tools", () => {
24
+ const tools = createMnemoTools({ apiKey: "k", workspaceId: "w" });
25
+ expect(tools.memorySearch).toBeDefined();
26
+ expect(tools.memoryAdd).toBeDefined();
27
+ });
28
+
29
+ it("memorySearch.execute calls client.search with default limit", async () => {
30
+ const tools = createMnemoTools({
31
+ apiKey: "k",
32
+ workspaceId: "w",
33
+ defaultLimit: 7,
34
+ });
35
+ const out = await tools.memorySearch.execute!(
36
+ { query: "oat milk" },
37
+ { messages: [], toolCallId: "t1" },
38
+ );
39
+ expect(out).toEqual({
40
+ results: [{ id: "m1", content: "user likes oat milk" }],
41
+ });
42
+ });
43
+
44
+ it("memoryAdd.execute merges base metadata with per-call metadata", async () => {
45
+ const tools = createMnemoTools({
46
+ apiKey: "k",
47
+ workspaceId: "w",
48
+ metadata: { userId: "u1" },
49
+ });
50
+ const out = await tools.memoryAdd.execute!(
51
+ { content: "likes oat milk", metadata: { topic: "drinks" } },
52
+ { messages: [], toolCallId: "t2" },
53
+ );
54
+ expect(out).toEqual({ memory: { id: "m2", content: "stored" } });
55
+ });
56
+
57
+ it("throws if apiKey missing and env unset", () => {
58
+ const orig = process.env.GETMNEMO_API_KEY;
59
+ delete process.env.GETMNEMO_API_KEY;
60
+ expect(() => createMnemoTools({})).toThrow(/missing apiKey/);
61
+ if (orig) process.env.GETMNEMO_API_KEY = orig;
62
+ });
63
+ });
package/src/tools.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { Mnemo } from "getmnemo";
2
+ import { tool } from "ai";
3
+ import { z } from "zod";
4
+
5
+ /**
6
+ * Options for constructing a Mnemo toolset.
7
+ *
8
+ * Either pass `client` (a pre-built `Mnemo` instance) or `apiKey` +
9
+ * `workspaceId` and one will be created for you.
10
+ */
11
+ export interface MnemoToolsOptions {
12
+ client?: Mnemo;
13
+ apiKey?: string;
14
+ workspaceId?: string;
15
+ /** Default `limit` passed to `search` when the model omits it. */
16
+ defaultLimit?: number;
17
+ /** Static metadata merged into every `add` call (e.g. `{ userId }`). */
18
+ metadata?: Record<string, unknown>;
19
+ }
20
+
21
+ export interface MnemoToolset {
22
+ memorySearch: ReturnType<typeof tool>;
23
+ memoryAdd: ReturnType<typeof tool>;
24
+ }
25
+
26
+ /**
27
+ * Build a pair of Vercel AI SDK tools backed by Mnemo.
28
+ *
29
+ * Drop the returned object into `streamText({ tools })` or
30
+ * `generateText({ tools })` and the model can search and write
31
+ * persistent memory.
32
+ */
33
+ export function createMnemoTools(
34
+ options: MnemoToolsOptions = {},
35
+ ): MnemoToolset {
36
+ const client = resolveClient(options);
37
+ const defaultLimit = options.defaultLimit ?? 5;
38
+ const baseMetadata = options.metadata ?? {};
39
+
40
+ const memorySearch: any = tool({
41
+ description:
42
+ "Search the user's long-term memory for facts, preferences, or past conversations relevant to the current query. Returns the most relevant snippets.",
43
+ // .strict() emits additionalProperties:false so providers that honour
44
+ // strict JSON schema (OpenAI, Anthropic) reject hallucinated keys
45
+ // instead of silently dropping them at parse time.
46
+ parameters: z
47
+ .object({
48
+ query: z
49
+ .string()
50
+ .min(1)
51
+ .describe("Natural-language query describing what to recall."),
52
+ limit: z
53
+ .number()
54
+ .int()
55
+ .positive()
56
+ .max(50)
57
+ .optional()
58
+ .describe("Max number of memories to return."),
59
+ })
60
+ ,
61
+ execute: async ({ query, limit }) => {
62
+ // Clamp the limit defensively — the schema constrains the model,
63
+ // but a non-conforming provider response could still smuggle a
64
+ // huge value through and blow the context window.
65
+ const requested = limit ?? defaultLimit;
66
+ const safeLimit = Math.min(50, Math.max(1, Math.floor(requested)));
67
+ const results = await client.search({ query, limit: safeLimit });
68
+ return { results };
69
+ },
70
+ });
71
+
72
+ const memoryAdd: any = tool({
73
+ description:
74
+ "Save a new fact, preference, or noteworthy detail about the user to long-term memory. Use sparingly — only for information worth remembering across sessions.",
75
+ parameters: z
76
+ .object({
77
+ content: z
78
+ .string()
79
+ .min(1)
80
+ .describe("The fact or note to remember, written in plain text."),
81
+ metadata: z
82
+ .record(z.unknown())
83
+ .optional()
84
+ .describe("Optional structured tags (e.g. { topic, source })."),
85
+ })
86
+ ,
87
+ execute: async ({ content, metadata }) => {
88
+ // Model-supplied metadata is merged FIRST so trusted baseMetadata
89
+ // (e.g. userId, workspaceId) cannot be overwritten by prompt injection.
90
+ const merged = { ...(metadata ?? {}), ...baseMetadata };
91
+ const memory = await client.add({ content, metadata: merged });
92
+ return { memory };
93
+ },
94
+ });
95
+
96
+ return { memorySearch, memoryAdd };
97
+ }
98
+
99
+ /**
100
+ * Pre-built default toolset using `GETMNEMO_API_KEY` and
101
+ * `GETMNEMO_WORKSPACE_ID` from `process.env`.
102
+ *
103
+ * Lazy — the client isn't constructed until a tool actually runs.
104
+ */
105
+ export const getmnemoTools: MnemoToolset = (() => {
106
+ let cached: MnemoToolset | null = null;
107
+ const get = () => (cached ??= createMnemoTools());
108
+ return new Proxy({} as MnemoToolset, {
109
+ get: (_target, prop: string) => get()[prop as keyof MnemoToolset],
110
+ });
111
+ })();
112
+
113
+ function resolveClient(opts: MnemoToolsOptions): Mnemo {
114
+ if (opts.client) return opts.client;
115
+ const apiKey = opts.apiKey ?? process.env.GETMNEMO_API_KEY;
116
+ const workspaceId = opts.workspaceId ?? process.env.GETMNEMO_WORKSPACE_ID;
117
+ if (!apiKey || !workspaceId) {
118
+ throw new Error(
119
+ "createMnemoTools: missing apiKey/workspaceId. Pass them explicitly or set GETMNEMO_API_KEY and GETMNEMO_WORKSPACE_ID.",
120
+ );
121
+ }
122
+ return new Mnemo({ apiKey, workspaceId });
123
+ }