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.
| Language | Install | Import |
|---|---|---|
| Python 3 | pip install gnarl-client | from gnarl import Client |
| TypeScript / JavaScript | npm install gnarl-client | import { GnarlClient } from "gnarl-client" |
| Go | go get github.com/gnarl-dev/go-client | import 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:
$GNARL_URL, if it is set;- the address the node on this machine recorded when it started, in
runtime/endpoint.jsonunder$LUCENIA_DATA_DIR, else under~/.lucenia— the same place thegnarlcommand looks, so a node on another port is still found; 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,
useris a partition, not access control. Recall for one user never returns another's memories, but a caller on the device can pass anyuser. 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 with403. See Several people on one mesh. - Pass
spaceornamespace, not both.space(personal, orhouseholdfor the team space) picks one of the node's built-in namespaces; give your app its ownnamespaceinstead. - Writes are not retried.
remembercould 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:
- python-client — sync and async
- ts-client — zero runtime dependencies; Node 18+, Bun, Deno, browsers
- go-client
Next
- Build your own agent — a starter template and one prompt for your coding assistant
- Agent memory — memory for the assistant itself, over MCP
- Query API — the HTTP API the clients wrap