Skip to main content

Client libraries

Three open-source clients (Apache-2.0), one surface: search, indexes, documents and agent memory. With the Gnarly app running on the same machine, none of them needs an address.

LanguageInstallImport
Python 3pip install gnarl-clientfrom gnarl import Client
TypeScript / JavaScriptnpm install gnarl-clientimport { GnarlClient } from "gnarl-client"
Gogo get github.com/gnarl-dev/go-clientimport gnarl "github.com/gnarl-dev/go-client"

The Python package is gnarl-client. The bare name gnarl on PyPI belongs to an unrelated project.

No address to configure​

Create a client with no address and it looks for a node in this order:

  1. $GNARL_URL, if it is set;
  2. the address the node on this machine recorded when it started, in runtime/endpoint.json under $LUCENIA_DATA_DIR, else under ~/.lucenia — the same place the gnarl command looks, so a node on another port is still found;
  3. http://127.0.0.1:43300, where the Gnarly desktop app listens.

So this is the whole setup:

from gnarl import Client

with Client() as c:
print(c.memory.recall("what did we decide about the schema?", k=3).memories)
import { GnarlClient } from "gnarl-client";

const gnarl = new GnarlClient();
c, err := gnarl.New("")

The recorded address is a hint, not an authority: a plain-HTTP one is used only when it points at loopback, so a stray file can never send your requests across a network in the clear. An address you pass yourself is used exactly as given, and an https one is never downgraded.

Agent memory​

Recall before answering, remember after. Give each app its own namespace, each person their own user, and each conversation a session. Embedding happens on the node.

from gnarl import Client

with Client() as c:
c.memory.remember("Alice prefers window seats on morning flights",
namespace="my-agent", user="alice", session="chat-42")
res = c.memory.recall("how does Alice like to fly?",
namespace="my-agent", user="alice", k=5)
for m in res.memories:
print(f"{m.score:.2f} {m.content}")
import { GnarlClient } from "gnarl-client";

const gnarl = new GnarlClient();
await gnarl.memory.remember({
content: "Alice prefers window seats on morning flights",
namespace: "my-agent", user: "alice", session: "chat-42",
});
const { memories } = await gnarl.memory.recall({
query: "how does Alice like to fly?", namespace: "my-agent", user: "alice", k: 5,
});
for (const m of memories) console.log(m.score.toFixed(2), m.content);
package main

import (
"context"
"fmt"
"log"

gnarl "github.com/gnarl-dev/go-client"
)

func main() {
ctx := context.Background()
c, err := gnarl.New("")
if err != nil {
log.Fatal(err)
}
scope := gnarl.MemoryScope{Namespace: "my-agent", User: "alice", Session: "chat-42"}
if _, err := c.Remember(ctx, gnarl.RememberRequest{
MemoryScope: scope,
Content: "Alice prefers window seats on morning flights",
}); err != nil {
log.Fatal(err)
}
got, err := c.Recall(ctx, gnarl.RecallRequest{
MemoryScope: gnarl.MemoryScope{Namespace: "my-agent", User: "alice"},
Query: "how does Alice like to fly?",
K: 5,
})
if err != nil {
log.Fatal(err)
}
for _, m := range got.Memories {
fmt.Printf("%.2f %s\n", m.Score, m.Content)
}
}

A few rules worth knowing before you write the second call:

  • Without a token, user is a partition, not access control. Recall for one user never returns another's memories, but a caller on the device can pass any user. In a multi-user service, set it from your own authenticated identity. A request that carries a capability token is different: the node binds it to the token's user and refuses any other with 403. See Several people on one mesh.
  • Pass space or namespace, not both. space (personal, or household for the team space) picks one of the node's built-in namespaces; give your app its own namespace instead.
  • Writes are not retried. remember could land twice if a lost response were retried, and the node does not de-duplicate, so recall before storing again. Reads are retried on 429 and 503.

The complete memory reference, written for an LLM to read, is llms-full.txt.

A node somewhere else​

Pass the address, and a token if the node runs access control. A node with RBAC exempts loopback callers, so a local node needs none.

from gnarl import Client

c = Client("https://node.example.com", token="...")
import { GnarlClient } from "gnarl-client";

const gnarl = new GnarlClient({ url: "https://node.example.com", token: process.env.GNARL_TOKEN });
c, err := gnarl.New("https://node.example.com", gnarl.WithToken(os.Getenv("GNARL_TOKEN")))

With no token argument, each client reads $GNARL_TOKEN.

A node you started with gnarl start​

gnarl start serves HTTPS on port 43300 with a self-signed certificate, and the clients keep certificate verification on. Either start it with --no-tls for local development, or turn verification off for that node only — never for anything you did not start yourself:

c = Client("https://localhost:43300", verify=False)
c, err := gnarl.New("https://127.0.0.1:43300", gnarl.WithInsecureSkipVerify())

The TypeScript client takes a fetch or undici agent for this; its README has the line for Node, Bun, Deno and browsers.

Full references​

Each README covers the whole surface — indexes, search, bulk, namespaces, backup and restore, and the error types:

Next​