{"id":47928285,"url":"https://github.com/germanamz/tusk","last_synced_at":"2026-06-29T21:00:30.512Z","repository":{"id":348937974,"uuid":"1199048810","full_name":"germanamz/tusk","owner":"germanamz","description":"Local-first agent brain — a markdown vault indexed as a schema-validated, semantically-searchable graph, queryable from the CLI or any MCP-compatible agent.","archived":false,"fork":false,"pushed_at":"2026-06-23T22:43:30.000Z","size":7473,"stargazers_count":3,"open_issues_count":7,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-24T00:21:50.014Z","etag":null,"topics":["agent-memory","ai-agents","cli","embeddings","golang","knowledge-graph","local-first","markdown","mcp","mcp-server","semantic-search","sqlite"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/germanamz.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/roadmap.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-02T02:22:04.000Z","updated_at":"2026-06-23T22:41:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/germanamz/tusk","commit_stats":null,"previous_names":["germanamz/tusk"],"tags_count":39,"template":false,"template_full_name":null,"purl":"pkg:github/germanamz/tusk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/germanamz%2Ftusk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/germanamz%2Ftusk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/germanamz%2Ftusk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/germanamz%2Ftusk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/germanamz","download_url":"https://codeload.github.com/germanamz/tusk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/germanamz%2Ftusk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34942665,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-29T02:00:05.398Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-memory","ai-agents","cli","embeddings","golang","knowledge-graph","local-first","markdown","mcp","mcp-server","semantic-search","sqlite"],"created_at":"2026-04-04T07:02:39.401Z","updated_at":"2026-06-29T21:00:30.498Z","avatar_url":"https://github.com/germanamz.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tusk\n\n**A local-first agent brain.** Tusk turns a directory of markdown files into a\nschema-validated, semantically-indexed graph — queryable from the CLI and from\nany MCP-compatible agent (Claude Code, Cursor, etc.).\n\nFiles are the source of truth. Git is the history. Tusk is the indexer and the\nretrieval engine.\n\n```\nmarkdown vault  ──▶  tusk indexer  ──▶  SQLite graph + embeddings\n                                              │\n                                              ├─▶ CLI       (tusk query, tusk node …)\n                                              └─▶ MCP tools (tusk_query, tusk_node_create, …)\n```\n\n- **Local first.** No service to log in to. The index lives in `.tusk/` next to your files.\n- **Schema-validated.** Node and edge types are declared in `tusk.toml`. Off-schema content is warned, never rejected.\n- **Structural + semantic.** A compact filter grammar for the graph (`key=value` / `key:value`, ranges, edge traversal, boolean composition), Ollama-backed embeddings for similarity, and a hybrid mode that filters then ranks.\n- **External edits are first-class.** Vim, Obsidian, an LLM piping markdown — they all work; the watcher keeps the index live.\n- **One engine, two surfaces.** Every read/write graph verb has a 1:1 MCP tool; workspace bootstrap (`tusk init`) and the graph viewer (`tusk graph`) stay CLI-only.\n\n---\n\n## Installation\n\n### Prerequisites\n\n- (Optional, for semantic search) [Ollama](https://ollama.com) running locally with an embedding model, e.g. `ollama pull nomic-embed-text`.\n\n### One-liner (prebuilt binary)\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/germanamz/tusk/main/install.sh | sh\n```\n\nDetects your OS/arch, downloads the latest GitHub release, drops the `tusk` binary into `~/.local/bin` (override with `INSTALL_DIR=/usr/local/bin`), and installs its man pages into `~/.local/share/man` (override with `MAN_DIR`; `man tusk` works once that dir is on your `MANPATH`). Pin a specific release with `TUSK_VERSION=v1.1.0`. Prebuilt archives ship for darwin/linux/windows on amd64 + arm64.\n\n### From source\n\nRequires Go 1.26+.\n\n```bash\ngit clone https://github.com/germanamz/tusk\ncd tusk\nmake build\n# binary at ./bin/tusk — move it onto your PATH\ninstall bin/tusk /usr/local/bin/tusk\n```\n\nOr, without cloning:\n\n```bash\ngo install github.com/germanamz/tusk/cmd/tusk@latest\n```\n\nVerify:\n\n```bash\ntusk --version\n```\n\n### Updating\n\nTusk has no self-update command — re-run whichever install method you used and the binary gets overwritten in place. Your workspace and `.tusk/` index are untouched.\n\n```bash\n# Prebuilt binary — same one-liner; defaults to the latest release\ncurl -fsSL https://raw.githubusercontent.com/germanamz/tusk/main/install.sh | sh\n\n# Pin a specific version\ncurl -fsSL https://raw.githubusercontent.com/germanamz/tusk/main/install.sh | TUSK_VERSION=v1.2.0 sh\n\n# From source\ncd tusk \u0026\u0026 git pull \u0026\u0026 make build \u0026\u0026 install bin/tusk /usr/local/bin/tusk\n\n# go install\ngo install github.com/germanamz/tusk/cmd/tusk@latest\n```\n\nAfter a major upgrade, run `tusk reindex` to pick up any indexer changes, then `tusk doctor` to confirm the workspace is healthy.\n\n---\n\n## Quickstart\n\n\u003e For per-command reference (flags, examples), see\n\u003e [docs/cli/](docs/cli/README.md). For multi-command recipes, see\n\u003e [docs/cli/workflows.md](docs/cli/workflows.md). Man pages are in [`man/`](man/) — `man -M man tusk` after cloning.\n\n```bash\n# 1. Initialize a workspace in the current directory\nmkdir my-brain \u0026\u0026 cd my-brain\ntusk init --name my-brain\n\n# 2. Add a built-in type pack so you have some node types\ntusk pack add vault     # note, meeting, decision + references/relates-to edges\ntusk pack add tags      # tag node type + tagged edge\ntusk pack add kanban    # ticket node type + workflow + parent/blocks edges\n\n# 3. Create a node\ntusk node create --path notes/hello.md --type note --title \"Hello, Tusk\"\n\n# 4. Build the index (also runs after every CLI write)\ntusk reindex\n\n# 5. Query the graph\ntusk node list 'type=note'\ntusk query 'type=ticket status=active' --sort '+priority,-due'\n\n# 6. Get a quick health check\ntusk status\ntusk doctor\n```\n\n---\n\n## How information is structured\n\nA Tusk workspace is just a directory:\n\n```\nmy-brain/\n├── tusk.toml                # workspace manifest (committed)\n├── .tusk/                   # gitignored — local SQLite index\n│   └── tusk.db\n├── .gitignore\n├── notes/\n│   └── auth-rfc.md\n├── tickets/\n│   └── fix-login-bug.md\n└── tags/\n    └── auth.md\n```\n\n### Nodes are markdown files\n\nEvery `.md` file with a `type:` field in YAML frontmatter is a **node**. The file path (minus the extension) is the canonical **node id** — no separate id field.\n\n```markdown\n---\ntype: ticket\ntitle: Fix login bug\nstatus: active\npriority: high\ndue: 2026-05-15\nparent: tickets/auth-epic\nblocks: [tickets/refactor-storage]\ntags: [auth, security]\n---\n\n# Fix login bug\n\nThe bug occurs when users with SSO accounts hit the password reset flow.\nSee [[notes/auth-rfc]] for context.\n```\n\n- `type` is the only universally reserved key.\n- Other frontmatter keys are either **properties** (string / int / date / enum / ref / list-of) or **edges** (declared in `tusk.toml`).\n- `[[notes/auth-rfc]]` body wikilinks materialize as edges to that node id for any edge type declared with `wikilinks = true` (e.g. the `vault` pack's `references` edge).\n\n### Edges connect nodes\n\nEdges are typed, declared in the manifest, and can be created two ways:\n\n- **Frontmatter** — the natural place. `parent: tickets/auth-epic` declares a `parent` edge.\n- **CLI / MCP** — `tusk edge add --type blocks --source tickets/a --target tickets/b`.\n\nEdge declarations enforce legality (`from`/`to` types), cardinality, ordering, and optional `acyclic = true` (cycles are rejected at write time).\n\n### The manifest defines the schema\n\n`tusk.toml` is the contract between you and the engine. A minimal manifest:\n\n```toml\n[workspace]\nname = \"my-brain\"\nignore = [\"bin/\", \"node_modules/\", \"*.test\"]\n\n[embeddings]\nprovider = \"ollama\"\nendpoint = \"http://localhost:11434\"\nmodel    = \"nomic-embed-text\"\ndim      = 768\n\n[node-types.note]\ndescription = \"A free-form markdown note\"\nproperties = []\n\n[node-types.decision]\ndescription = \"A captured decision\"\nproperties = [\n    { name = \"decided-at\",  type = \"date\", required = true },\n    { name = \"status\",      type = \"enum\", values = [\"proposed\", \"accepted\", \"rejected\", \"superseded\"] },\n    { name = \"supersedes\",  type = \"ref\",  to = \"decision\" },\n]\n\n[edge-types.references]\ndescription = \"Implicit edge materialized from body wikilinks\"\nfrom        = [\"*\"]\nto          = [\"*\"]\ncardinality = \"many-to-many\"\ninverse     = \"referenced-by\"\n```\n\n`ref` properties auto-materialize edge types of the same name — declaring `supersedes` as a `ref` to `decision` gives you a `supersedes` edge for free.\n\nEdited `tusk.toml` while a daemon is running? `tusk reload` (or the `tusk_reload` MCP tool) re-reads and validates the manifest, hot-swaps the schema in place — no restart — and converges any sibling daemons via the `.tusk/manifest-epoch` sentinel. It then reindexes to re-validate your content against the new schema. Validation matches startup, so a reload lands the same state a restart would.\n\n### Type packs (built-in templates)\n\nInstead of declaring everything by hand, `tusk pack add \u003cname\u003e` splices a curated TOML block into your manifest:\n\n| Pack | Adds |\n|------|------|\n| `vault` | `note`, `meeting`, `decision`; `references` (wikilinks) + `relates-to` |\n| `tags` | `tag` node + `tagged` edge (with `tags: [a, b]` frontmatter shorthand) |\n| `kanban` | `ticket` node with workflow-validated `status`; `parent` (WBS) + `blocks` edges |\n| `dev` | `spec`, `plan`, `handoff`, `package` — dogfooding pack for tracking software projects |\n\nPacks compose: add `vault` + `tags` + `kanban` and you have notes, decisions, tags, and a kanban workflow on top.\n\n```bash\ntusk pack add vault\ntusk pack add tags\ntusk pack add kanban\n```\n\nYou can also load a pack from a URL or local file:\n\n```bash\ntusk pack add https://example.com/packs/research.toml\ntusk pack add file://$PWD/my-pack.toml\n```\n\n---\n\n## Indexing\n\nThe index lives in `.tusk/tusk.db` (SQLite + WAL). It is **derived state** — delete `.tusk/` and `tusk reindex` rebuilds it identically.\n\n### One-shot reindex\n\n```bash\ntusk reindex\n# Reindex done: 142 indexed, 0 removed, 3 skipped\n```\n\n`reindex` walks the workspace, parses every `.md`, validates frontmatter against the manifest, resolves refs + wikilinks into edges, and enqueues embeddings.\n\n### Live watcher\n\n```bash\ntusk watch\n```\n\nRuns fsnotify against the workspace and applies edits incrementally. Drains the embed queue in the background.\n\n### What gets indexed\n\n- Every `.md` file with a `type:` field, anywhere in the workspace.\n- Every `.html` / `.htm` file with a `\u003cmeta name=\"tusk:type\"\u003e` tag — indexed\n  over its **prose** (tags stripped, entities decoded), with `\u003cmeta name=\"tusk:*\"\u003e`\n  becoming typed node properties and `data-*` attributes captured as lenient\n  signals under the reserved `data` key.\n- Filtered through `.gitignore` + `[workspace] ignore` patterns.\n- `.tusk/` and `.git/` are always ignored.\n\nOff-schema content is **warned, not rejected** — a file with an unknown `type:` or a property violation still gets indexed (so it stays queryable) and surfaces in `tusk doctor`.\n\n---\n\n## Querying\n\n### Structural filter\n\nA compact filter grammar that compiles to parameterized SQL. Property\ncomparisons accept `=` or `:` interchangeably; the rest of the grammar uses\nthe operators below.\n\n```bash\n# Property predicates (`=` and `:` are equivalent)\ntusk query 'type=ticket status=active priority=high'\ntusk query 'type:note created\u003e=2026-04-01'\n\n# Edge traversal: -\u003e outgoing, \u003c- incoming\ntusk query 'type=ticket blocks-\u003etype=ticket'        # tickets that block other tickets\ntusk query 'type=note \u003c-references type=spec'      # notes referenced by specs\n\n# Multi-hop\ntusk query 'type=ticket parent-\u003eparent-\u003etitle=\"auth-epic\"'\n\n# Sort + pagination\ntusk query 'type=ticket status=active' --sort '+priority,-due' --take 10\n```\n\n### Semantic search\n\nRequires `[embeddings]` configured (Ollama by default). Embedding runs asynchronously after writes; until a node is embedded, it's invisible to semantic queries (and surfaces in `tusk doctor`).\n\n```bash\ntusk query 'type=*' --semantic \"auth bug in password reset flow\" --take 5\n```\n\n### Hybrid (recommended for agents)\n\nStructural filter narrows the candidate set; semantic similarity ranks within it.\n\n```bash\ntusk query 'type=ticket status=active' --semantic \"login flow\" --take 10\n```\n\n### JSON output\n\nAdd `--json` to any query for structured output that's easy to pipe into an LLM or a script.\n\n```bash\ntusk query 'type=decision' --semantic \"storage backend\" --top 3 --json\n```\n\n---\n\n## MCP server (Claude Code, Cursor, …)\n\n`tusk mcp` runs an MCP server backed by the same indexing engine as the CLI, exposing the graph verbs as tools. Agents should prefer these tools over shelling out to `tusk` — they run in the warm daemon with the index already open. Stdio is the default transport; SSE is available on a port.\n\n```bash\ntusk mcp                      # stdio (for Claude Code / Cursor / Codex)\ntusk mcp --transport sse --addr :8765\n```\n\nIt holds the workspace open for the lifetime of the session: a single SQLite handle, an embed-queue drainer, and an fsnotify watcher all live in the same process so the index stays warm across tool calls.\n\n### Wiring it into Claude Code\n\n```bash\nclaude mcp add tusk -- /usr/local/bin/tusk mcp\n```\n\nOr directly in `~/.claude.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"tusk\": {\n      \"command\": \"/usr/local/bin/tusk\",\n      \"args\": [\"mcp\"],\n      \"cwd\": \"/path/to/my-brain\"\n    }\n  }\n}\n```\n\n### Available MCP tools\n\n| Tool | What it does |\n|------|--------------|\n| `tusk_status` | node counts by type, edge count, queue depth, last reindex |\n| `tusk_doctor` | validation warnings, dangling refs, embed-queue retries |\n| `tusk_node_get` / `tusk_node_list` | read by id or filter |\n| `tusk_node_render` | render a node's content as plain text (HTML tags / markdown markup stripped) |\n| `tusk_node_create` / `tusk_node_modify` / `tusk_node_move` / `tusk_node_delete` | write |\n| `tusk_edge_add` / `tusk_edge_remove` / `tusk_edge_list` | edge CRUD |\n| `tusk_query` | structural + optional `semantic` ranking |\n| `tusk_context` | composed warm-context digest (pinned nodes, recent activity, aliases) |\n| `tusk_run` | invoke a manifest-declared alias by name |\n| `tusk_reindex` | force a full walk |\n| `tusk_reload` | hot-reload `tusk.toml`: validate + swap the schema, no restart |\n| `tusk_reset` | drop and rebuild the index from files (`confirm: true`) |\n| `tusk_pack_add` | merge a built-in type pack's node/edge types into `tusk.toml` and hot-reload the schema |\n\nWorkspace bootstrap (`tusk init`) and the graph viewer (`tusk graph`) stay CLI-only.\n\n---\n\n## Health and diagnostics\n\n```bash\ntusk status     # node counts, edge count, embed-queue depth, last reindex timestamp\ntusk doctor     # validation warnings, dangling refs/wikilinks, embed-queue errors, embed stats\n```\n\n`doctor` is the place to look when:\n\n- semantic queries seem to be missing nodes → check the embed-queue depth and last error\n- a wikilink points to nothing → dangling-ref warning surfaces it\n- a manifest change just landed → re-validate every affected node\n\n---\n\n## Architecture\n\nSingle Go binary, single SQLite index, single embedding provider (Ollama for now).\n\n```mermaid\nflowchart TD\n    workspace[\"Workspace\u003cbr/\u003e(markdown + tusk.toml)\"]\n    engine[\"Engine (cmd/tusk + internal/*)\u003cbr/\u003emanifest · node · edge · reindex · filter · embed\u003cbr/\u003ewatcher · behaviors · mcp\"]\n    db[\".tusk/tusk.db\u003cbr/\u003e(SQLite WAL, embeddings table)\"]\n\n    workspace --\u003e|\"fs walk / fsnotify\"| engine\n    engine --\u003e|\"reads / writes\"| db\n```\n\n- **Filesystem \u003e index, always.** The index is a cache; if it is stale, wedged, or corrupt, run `tusk reset` (or the `tusk_reset` MCP tool with `confirm: true`) to drop and rebuild it from your files. The markdown files are the source of truth, so nothing is lost.\n- **Stateless across machines.** Clone the vault, reindex, get an identical brain.\n- **Single-writer, many-readers.** SQLite WAL + a workspace-wide advisory lock so `tusk mcp` and one-shot CLI calls coexist.\n\nProduct vision and design principles live in [`PRODUCT.md`](PRODUCT.md). Per-package notes live in [`docs/packages/`](docs/packages/).\n\n---\n\n## Development\n\n```bash\nmake build        # ./bin/tusk\nmake test         # unit tests\nmake test-race    # with race detector\nmake vet\nmake lint         # golangci-lint\nmake fmt\n```\n\nSee [`STYLE.md`](STYLE.md) for the codebase conventions and [`CONTRIBUTING.md`](CONTRIBUTING.md) for how to propose changes.\n\n## License\n\n[Apache 2.0](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgermanamz%2Ftusk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgermanamz%2Ftusk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgermanamz%2Ftusk/lists"}