thrift-memory

Thrift Memory é um servidor de memória MCP focado em custo para agentes de codificação que param de recarregar grandes arquivos MEMORY.md, AGENTS.md e de contexto de projeto a cada sessão. Ele recupera apenas a memória relevante para a tarefa dentro de um orçamento rígido de tokens e retorna um recibo de economia: baselineTokens vs injectedTokens vs savedTokens.

Documentação

Thrift Memory

O servidor de memória MCP que prova quantos tokens você economizou. (npm: thrift-memory)

🌐 Página inicial do thrift-memory →  ·  npm

Não afiliado ao Apache Thrift, o framework RPC. Este projeto é sempre referido como Thrift Memory — uma camada de memória MCP para agentes de codificação.

Thrift Memory é um servidor de memória MCP com foco em custo para agentes de codificação que param de recarregar grandes arquivos de MEMORY.md, AGENTS.md e contexto de projeto a cada sessão. Ele recupera apenas memória relevante à tarefa sob um orçamento rígido de tokens e retorna um recibo de economia: baselineTokens vs injectedTokens vs savedTokens.

savedTokens = baselineTokens - injectedTokens

Se o seu agente de codificação recarrega o mesmo arquivo de contexto grande a cada início de sessão, essa recarga é um custo de token puro e repetido. O Thrift Memory limita isso e — de forma única — registra um recibo a cada recuperação para que você veja o uso de tokens que evitou, não apenas confie que evitou.

Recuperação com orçamento, em uma linha: Thrift Memory recupera apenas memória relevante à tarefa sob um orçamento rígido de tokens e registra um recibo mostrando baselineTokens vs injectedTokens vs savedTokens.

Status: 0.0.x inicial. As APIs são úteis, mas ainda podem mudar antes de v0.1.

O que ele faz

O Thrift tem três superfícies:

SuperfíciePropósito
Servidor MCPFerramentas de memória do agente: remember, recall, search_memory
Painel localUI de economia apoiada pelo JSONL do medidor, além de controles do proprietário (fixar/desativar, orçamentos, kill-switch)
ProxyGateway HTTP opcional que ajusta solicitações LLM ao vivo e tenta novamente limites de taxa

Seja preciso sobre a divisão:

  • MCP gerencia recuperação de memória e recibos de tokens.
  • thrift-proxy gerencia o ajuste de solicitações ao vivo e novas tentativas de limite de taxa.

Como se compara

A comparação certa para o Thrift Memory não é com camadas de qualidade de recuperação / grafo de conhecimento como Mem0, Zep ou Graphiti — essas otimizam o quão inteligente é a recuperação. O Thrift Memory compete com o conjunto crescente de servidores de memória MCP para agentes de codificação, e difere de todos eles em um eixo: visibilidade de custo.

Cada recuperação retorna um recibo de economia — baselineTokens, injectedTokens, savedTokens — para que você veja quantos tokens evitou. Nenhum outro servidor nesta categoria se posiciona em torno de provar a economia.

ServidorO que otimizaOrçamento rígido de tokens na recuperação?Emite um recibo de economia (baseline vs injected vs saved)?
Thrift MemoryRecuperação com foco em custo — limite os tokens e prove a economiaSimSim — a cada recuperação
Official Memory MCPMemória de grafo de conhecimento (entidades / relações)NãoNão
Context ModeSandbox de contexto — mantém saídas grandes de ferramentas/arquivos fora do contexto (SQLite FTS5)Não (sandbox, não um orçamento de recuperação)Não
Agent Memory MCPRetorna um pequeno índice via memory_read, depois memory_search por tópicoNãoNão
@provos/memory-mcp-serverRecuperação memory_context / task dentro de um orçamento de tokensSimNão
memento-memory-mcpMemória para agentes de codificação — importa CLAUDE.md, SQLite, sincronização git, UI localNãoNão
MCP Context ServerArmazenamento com escopo de thread, busca de texto completo / semântica / híbrida, reclassificaçãoNãoNão
smart-claude-memory-mcpArmazenamento de memória orientado a ClaudeNãoNão

O concorrente mais próximo, @provos/memory-mcp-server, também recupera sob um orçamento de tokens — mas não mostra o que o orçamento economizou para você. O diferencial do Thrift Memory não é "eu faço memória"; é "eu faço memória com contabilidade de custo." O recibo savedTokens = baselineTokens - injectedTokens é a coisa que ninguém mais nesta categoria lidera.

Resumo honesto: se você precisa da recuperação mais inteligente possível, use uma camada de grafo de conhecimento como Mem0 ou Zep. Se seus agentes de codificação continuam pagando para recarregar grandes arquivos de MEMORY.md / AGENTS.md / contexto de projeto a cada início de sessão e você quer medir e limitar esse custo sem infraestrutura extra, essa lacuna é o que o Thrift Memory preenche. Os dois não são mutuamente exclusivos — o Thrift Memory pode ficar na frente de um armazenamento mais pesado como a camada de orçamento/medição.

Para a comparação completa — incluindo como o Thrift Memory difere de Mem0, Zep e Graphiti no eixo custo vs qualidade de recuperação — veja docs/COMPARISON.md. Perguntas comuns são respondidas em docs/FAQ.md. Para um passeio narrativo de todo o campo de memória — camadas de qualidade de recuperação vs. os servidores de memória MCP com foco em custo — leia o post do blog Mem0 vs Zep vs Graphiti.

Ferramentas MCP

remember(scope, text, agentId?, sessionId?, tags?)
  Store a memory in org, agent, or session scope.

recall(agentId, tokenBudget, task?, tags?)
  Return relevant memories under a hard token budget.
  Also returns { injectedTokens, baselineTokens, savedTokens }.

search_memory(agentId, task?, tags?, limit?)
  Browse matching memories without applying a small recall budget.

Veja seu próprio desperdício (10 segundos, nada instalado)

Antes de adotar qualquer coisa, meça o que seus agentes já recarregam a cada sessão:

npx -y thrift-memory audit

Ele escaneia o repositório atual em busca de arquivos de memória / instrução do agente — CLAUDE.md, CLAUDE.local.md, MEMORY.md, AGENTS.md, GEMINI.md, .cursorrules, .cursor/rules/, .windsurfrules, .clinerules, .github/copilot-instructions.md, além do seu ~/.claude/CLAUDE.md global do usuário — e imprime a conta:

Thrift Memory audit — D:\myrepo

  File                               Tokens
  CLAUDE.md                           3,000
  .cursor/rules/api.mdc                 900
  AGENTS.md                             800
  .github/copilot-instructions.md       300
  TOTAL reloaded per session          5,000

At 10 sessions/day (--sessions): ~50,000 tokens/day, ~1,500,000/month
≈ $22.50/month at $15/M input tokens (an assumption — adjust: --price-per-mtok)

With recall capped at 2,000 tokens/session (--budget): projected saving ~60%

Cada número é calculado a partir dos seus arquivos com o mesmo estimador que o medidor usa — nada é enviado para casa, nada é instalado. Flags: --path=, --sessions=, --budget=, --price-per-mtok=.

Início rápido

Opção A — Plugin do Claude Code (um comando, memória automática)

Se você usa Claude Code, instale tudo — servidor MCP, um agente com memória e comandos /thrift-recall / /thrift-remember — em um único passo:

/plugin marketplace add YohadH/thrift-memory
/plugin install thrift-memory@thrift

Isso registra o servidor MCP thrift automaticamente (via npx thrift-memory), então recall / remember / search_memory ficam disponíveis sem edição de configuração. Veja plugins/thrift-memory/ para o que o plugin inclui.

Memória automática (plugin v0.2.0): o plugin inclui um hook SessionStart que executa thrift-memory session-context e injeta uma fatia de memória com orçamento (padrão 1.500 tokens) diretamente no contexto a cada início de sessão, retomada, /clear e pós-compactação. Suas memórias duráveis sobrevivem à perda de contexto com zero chamadas de ferramenta — e cada auto-injeção é medida (agente session-start), então o painel mostra o que o caminho automático custa e economiza também. Um armazenamento vazio não injeta nada.

Opção B — Configuração MCP (qualquer cliente MCP)

npm install -g thrift-memory

Adicione o Thrift a um cliente compatível com MCP:

{
  "mcpServers": {
    "thrift": {
      "command": "npx",
      "args": ["thrift-memory"]
    }
  }
}

Ou execute o servidor MCP diretamente:

npx thrift-memory \
  --store-path=~/.thrift/memories.jsonl \
  --meter-path=~/.thrift/meter.jsonl \
  --default-budget=2000

Recuperação com suporte a arquivos + sobreposição JSONL

Por padrão, o servidor MCP também escaneia o diretório de trabalho atual em busca de arquivos de contexto do agente existentes: MEMORY.md, AGENTS.md, CLAUDE.md, GEMINI.md, .cursorrules, .windsurfrules, .clinerules, .cursor/rules/*.md|*.mdc, .windsurf/rules/*.md|*.mdc, .github/copilot-instructions.md e pastas específicas do agente que correspondem a memory/<agentId>/*.md.

Esses arquivos são tratados como fontes de recuperação somente leitura. remember() ainda grava novas memórias duráveis no armazenamento JSONL em --store-path, então o modelo de execução é:

MEMORY.md / AGENTS.md / rules files / memory/<agentId>/*.md  +  ~/.thrift/memories.jsonl
                       read-only sources                     +       writable overlay

Arquivos sob memory/<agentId>/*.md são carregados como memórias com escopo de agente, então memory/takshi/crm.md é visível para agentId: "takshi", enquanto memory/qa-manager/smoke.md é visível para agentId: "qa-manager". Pastas compartilhadas como memory/reports, memory/feed e memory/advice não são tratadas como IDs de agente.

Edições de arquivo são capturadas na próxima recuperação/busca. Para escanear uma raiz de projeto diferente, passe --file-root=/path/to/repo ou defina THRIFT_FILE_ROOT. Para desativar a recuperação com suporte a arquivos e usar apenas memórias JSONL, passe --file-memory=false ou defina THRIFT_FILE_MEMORY=0.

Demonstração de 60 segundos

Nenhum agente necessário — prove o loop remember → recall → receipt com a biblioteca. Salve como demo.mjs após npm install thrift-memory, depois node demo.mjs:

import { JsonlStore, ScopedRetriever } from "thrift-memory";

const store = new JsonlStore({ path: "./demo.jsonl" });
const now = Date.now();

// 1. remember — store a few org memories (cheap, no LLM enrichment)
store.add({ scope: "org", text: "All money values are stored as integer cents, never floats." }, now);
store.add({ scope: "org", text: "We deploy only on green CI; no Friday-evening releases." }, now);
store.add({ scope: "org", text: "Postgres is the system of record; Redis is cache-only." }, now);

// 2. recall — load only what the task needs, under a hard token budget
const r = new ScopedRetriever().recall(store, {
  agentId: "dev",
  task: "how should I store money values?",
  tokenBudget: 40,
});

// 3. receipt
for (const m of r.memories) console.log("•", m.text);
console.log(`injected ${r.injectedTokens} / baseline ${r.baselineTokens} (saved ${r.savedTokens})`);
• All money values are stored as integer cents, never floats.
injected 15 / baseline 43 (saved 28)

Apenas a memória relevante é injetada — as notas de cadência de deploy e Postgres são descartadas porque não correspondem à tarefa, não apenas por causa do orçamento (recall aplica um piso de relevância). Essa lacuna, baseline - injected, é exatamente o que você para de pagar a cada execução. A relevância aqui é sobreposição lexical, então formule a tarefa com palavras que suas memórias realmente usam; um resultado vazio significa que nada no escopo era relevante — o que é a resposta honesta, não ruído para preencher o orçamento.

Painel

O painel opcional é local. Ele mostra se o Thrift está realmente economizando tokens em execuções reais de agentes e (a partir de 0.0.3) expõe uma pequena superfície de escrita para controles do proprietário — fixar/desativar uma memória, definir orçamentos por agente, silenciar um agente e um kill-switch para toda a frota — por meio de endpoints locais POST/DELETE. Os mesmos controles estão disponíveis na CLI thrift-panel.

npx thrift-panel serve \
  --store-path=~/.thrift/memories.jsonl \
  --meter-path=~/.thrift/meter.jsonl \
  --control-path=~/.thrift/control.json \
  --port=8585

Abra http://127.0.0.1:8585.

Thrift dashboard

O painel mostra:

VisãoO que prova
Resumo da frotaTotal de tokens baseline, injetados, economizados e taxa de economia
Fluxo diário de tokensSe as economias persistem em dias reais
Economia por agenteQuais agentes são caros e quais economizam mais
Recibos recentesOs eventos mais recentes de recuperação/proxy medidos
Caminhos de auditoriaOs arquivos locais que sustentam os números

Equivalentes de CLI:

npx thrift-panel summary --store-path=~/.thrift/memories.jsonl --meter-path=~/.thrift/meter.jsonl
npx thrift-panel agents --store-path=~/.thrift/memories.jsonl --meter-path=~/.thrift/meter.jsonl
npx thrift-panel memories --store-path=~/.thrift/memories.jsonl --scope=org

Medindo desempenho

Cada recall grava um recibo em THRIFT_METER_PATH quando um caminho de medidor está configurado:

{"at":1760000000000,"agentId":"dev","injectedTokens":420,"baselineTokens":2100,"savedTokens":1680}

Definições:

CampoSignificado
baselineTokensO contrafactual sem Thrift: toda a memória no escopo que teria sido carregada
injectedTokensA fatia que o Thrift realmente retornou sob orçamento
savedTokensbaselineTokens - injectedTokens
Taxa de economiasavedTokens / baselineTokens

Loop de medição recomendado:

  1. Semeie memórias a partir dos seus próprios arquivos markdown ou use remember.
  2. Deixe agentes reais chamarem recall durante o trabalho normal.
  3. Revise thrift-panel summary e thrift-panel agents.
  4. Valide a qualidade separadamente comparando os resultados das tarefas com memória completa vs. recuperação do Thrift.

Para um relatório público confiável, publique tanto a redução de tokens quanto as evidências de qualidade. Por exemplo: "economizou 72% dos tokens de memória em 200 recuperações reais, com 19/20 tarefas pareadas produzindo o mesmo resultado."

Economizador de tokens seguro — sinais de pressão de orçamento

Cortar tokens só é seguro se o agente conseguir distinguir "recebi tudo que era relevante" de "recebi uma fração disso." Então, cada resultado de recall também relata quanto de memória relevante o orçamento forçou a deixar de fora:

{
  "injectedTokens": 492,
  "baselineTokens": 14000,
  "savedTokens": 13508,
  "relevantTokens": 2100,
  "skippedForBudget": 12,
  "skippedTokensForBudget": 1608,
  "hasMoreRelevantMemory": true,
  "budgetPressure": "high"
}
CampoSignificado
relevantTokensTokens de memória que passaram no filtro de relevância — o que valia a pena injetar antes de o orçamento ser aplicado
skippedForBudgetContagem de memórias relevantes descartadas apenas porque não cabiam no orçamento
skippedTokensForBudgetrelevantTokens - injectedTokens
hasMoreRelevantMemorytrue quando memória relevante foi deixada de fora por orçamento
budgetPressurenone (tudo relevante coube) · low · high (tanta memória relevante pulada quanto injetada)

Esses contam apenas memória que passou no filtro de relevância, então hasMoreRelevantMemory nunca dispara em ruído que a recuperação descartou corretamente. O loop pretendido é recuperação progressiva, feita pelo agente (não pelo usuário final): comece com um orçamento pequeno e, se budgetPressure for high, faça mais uma recuperação focada antes de agir — nunca excedendo um orçamento total de tarefa. É isso que transforma o Thrift de um economizador de tokens em um economizador de tokens seguro: você nunca age silenciosamente com uma fatia esgotada. O agente memory-keeper e o comando /thrift-recall do plugin do Claude Code já seguem esse loop.

Considere a sobrecarga do MCP. Registrar qualquer servidor MCP adiciona a carga do esquema de ferramentas ao contexto de cada agente (frequentemente vários milhares de tokens). O número honesto é líquido: savings = recall reduction − MCP schema/tool-call overhead. Em um agente com contexto pesado que recarrega memória ampla a cada execução, a recuperação geralmente vence por uma margem ampla — mas confirme com o medidor na sua própria carga de trabalho antes de adotar em toda a frota, em vez de presumir. Os recibos existem justamente para que você não precise adivinhar.

Benchmark Sintético

Este repositório inclui um pequeno fixture sintético para que os usuários possam verificar o pipeline de medição sem dados privados:

npm run build
node benchmark/run.mjs

Ele lê:

  • benchmark/fixtures/memories.jsonl
  • benchmark/fixtures/meter.jsonl

Veja docs/case-study.md para um exemplo sanitizado de como interpretar os números.

Monitoramento de Contexto

O hook UserPromptSubmit do plugin executa thrift-memory context-watch em cada prompt. Ele rastreia o uso de contexto em relação à janela do modelo e, quando o uso cruza um limite de etapa, injeta uma instrução dizendo ao agente para salvar fatos duráveis via remember e sugere executar /compact — para que as decisões sobrevivam à compactação em vez de serem descartadas silenciosamente.

O tamanho da etapa é limitado entre um piso e um teto para que não dispare com muita frequência em janelas pequenas nem raramente demais em janelas enormes:

step = clamp(stepPct% × window, minStepTokens, maxStepPct% × window)
FlagPadrãoSignificado
--step-pct=20Tamanho alvo da etapa, como porcentagem da janela
--min-step-tokens=80000Piso do tamanho da etapa, em tokens
--max-step-pct=50Teto do tamanho da etapa, como porcentagem da janela
--window-tokens=(auto)Substituir o tamanho da janela do modelo detectado
--state-path=~/.thrift/context-watch/Onde o estado de cruzamento de etapa é persistido

O ciclo salvar → compactar → recarregar: context-watch solicita um salvamento antes que um limite de etapa seja cruzado, PreCompact imprime orientação de compactação como rede de segurança, e o hook pré-existente SessionStart recarrega uma fatia de memória com orçamento imediatamente depois — fechando o ciclo para que nenhum fato durável seja perdido na compactação.

Salvamentos delta, não re-salvamentos: cada cruzamento agora marca sua orientação com um marcador específico da sessão, session:<sessionId>, para que o agente não receba apenas a instrução de "salvar fatos" às cegas toda vez. A instrução injetada faz o agente chamar search_memory para essa tag primeiro, a fim de ver o que já foi armazenado nesta sessão, e então salvar apenas fatos genuinamente novos, marcando-os da mesma forma. Isso impede que cruzamentos posteriores na mesma sessão relembrem o mesmo fato repetidamente e impede que o agente presuma incorretamente que algo já foi salvo.

Opte por não participar removendo as entradas UserPromptSubmit (e opcionalmente PreCompact) de plugins/thrift-memory/hooks/hooks.json.

Economia medida: node benchmark/context-watch.mjs mostra ~72,5% menos tokens recarregados em janelas simuladas (37.744 na linha de base vs. 10.367 injetados, economizando 27.377 tokens) — veja Benchmark Sintético acima para a metodologia.

Verificado

Testes de unidade. npm test — 157 testes em 12 arquivos, incluindo um test/contextWatch.test.ts dedicado que cobre a tabela de limites (tamanhos de etapa 1M→200k, 200k→80k, 128k→64k, 32k→16k), a máquina de estados de cruzamento de etapa (primeiro cruzamento dispara, a mesma etapa não dispara novamente, a próxima etapa dispara de novo, isolamento por sessão), análise do final do transcript (message.usage real, uma leitura limitada do final para transcripts grandes, fallback para fileSize / 4), inferência de modelo → janela, rejeição de path-traversal no ID de sessão e entrada malformada/ausente. Tudo verde.

Execução manual do contrato do hook. A CLI compilada (node dist/mcp/bin.js context-watch) foi acionada diretamente com JSON de stdin em formato de hook contra um transcript sintético: ela dispara com o JSON exato hookSpecificOutput em um cruzamento, permanece silenciosa em uma repetição da mesma etapa, dispara novamente na próxima etapa e permanece silenciosa (exit 0) em stdin inválido, stdin vazio e um caminho de transcript ausente — confirmando que o contrato de "nunca quebrar o prompt" se mantém em todos os modos de falha, não apenas no caminho feliz.

Execução real de ponta a ponta, contra a sessão de desenvolvimento desta própria funcionalidade. Em vez de apenas um fixture sintético, context-watch foi reproduzido contra o transcript real e ao vivo do Claude Code que foi gerado durante a construção desta funcionalidade — uma sessão genuinamente longa (963 KB, 410 linhas, dados reais de uso de claude-sonnet-5 / claude-fable-5, sem substituição de janela). Quatro snapshots reais foram extraídos desse transcript em pontos crescentes do histórico real da sessão e alimentados pela CLI em ordem cronológica, cada um como uma nova invocação de hook:

TurnoUso real (tokens)% da janela de 200kResultado
T131.686~16%silencioso (abaixo da primeira etapa)
T294.821~47%dispara — cruza a etapa de 40%
T3135.227~68%silencioso (mesma etapa que T2, sem novo disparo)
T4182.661~91%dispara — cruza a etapa de 80%

Isso corresponde exatamente ao comportamento documentado de "janela de 200k → salva em ~40% e ~80%", usando crescimento real de tokens por turno em vez de números escolhidos a dedo. O transcript completo de 963 KB (bem acima do limite de 64 KB da leitura limitada do final) também foi executado de forma independente e retornou em bem menos de um segundo (~0,3–0,6s de tempo real, dominado pela inicialização do processo Node, não pela análise do transcript) — confirmando que a leitura limitada do final mantém o hook barato mesmo contra uma sessão grande, real e de longa duração.

Proxy e Limites de Taxa

O proxy é opcional. Use-o quando um agente puder apontar seu base_url de LLM para um gateway HTTP local.

Segurança — execute apenas localmente. O proxy encaminha sua chave de API real do provedor upstream sem alterações. Ele vincula a 127.0.0.1 por padrão (aplicado no código, não apenas na documentação), portanto não é acessível fora do host, a menos que você opte deliberadamente com --host=0.0.0.0 / THRIFT_PROXY_HOST. Nunca o exponha em uma interface pública nem compartilhe a porta. É uma ferramenta de desenvolvimento de locatário único, não um gateway multiusuário endurecido. As respostas também são armazenadas em buffer, portanto o streaming SSE ainda não é repassado.

npx thrift-proxy \
  --upstream=https://api.anthropic.com \
  --host=127.0.0.1 \
  --port=8787 \
  --budget=4000 \
  --meter-path=~/.thrift/meter.jsonl

Em seguida, configure a URL base do LLM do agente como http://localhost:8787 e continue usando a chave de API real do provedor.

O proxy:

  • aparar o contexto de solicitação em tempo real sob um orçamento rígido de tokens,
  • grava os mesmos recibos de economia que a superfície MCP,
  • tenta novamente respostas upstream 429 e 503 Retry-After,
  • limita solicitações upstream concorrentes por provedor.

Padrões de limite de taxa:

ConfiguraçãoPadrãoVariável de ambiente
Concorrência máxima5THRIFT_MAX_CONCURRENCY
Máximo de tentativas5THRIFT_MAX_RETRIES
Base de backoff1000msTHRIFT_BACKOFF_BASE_MS
Backoff máximo60000msTHRIFT_MAX_BACKOFF_MS

thrift-proxy armazena respostas em buffer nesta versão; o repasse de streaming é uma melhoria futura.

Importar Memórias Existentes

O script de importação é genérico e apenas local. Ele pode importar arquivos markdown para um armazenamento JSONL:

node scripts/import-memories.mjs \
  --source=./memory \
  --scope=org \
  --store-path=~/.thrift/memories.jsonl \
  --dry-run

Para memórias com escopo de agente, coloque arquivos markdown em diretórios de projeto e use --scope=agent:

memory/
  checkout-service/
    dev.md
    qa.md
  docs-site/
    writer.md
node scripts/import-memories.mjs --source=./memory --scope=agent

Uso da Biblioteca

import { JsonlStore, ScopedRetriever, InMemoryMeter, ThriftMcpServer } from "thrift-memory";

const server = new ThriftMcpServer({
  store: new JsonlStore({ path: "./memories.jsonl" }),
  retriever: new ScopedRetriever(),
  meter: new InMemoryMeter(),
  defaultTokenBudget: 2000,
});

await server.runStdio();

Desenvolvimento

npm install
npm run typecheck
npm run build
npm test

Estrutura

CaminhoFinalidade
src/mcp/Servidor MCP stdio e definições de ferramentas
src/store/Armazenamento de memória JSONL
src/retrieval/Recuperação com escopo e orçamento limitado
src/meter/Medidor de tokens e agregações
src/control/CLI e painel local
src/proxy/Proxy HTTP, aparação de contexto, tentativas com limite de taxa
benchmark/fixtures/Dados públicos de benchmark sintético
docs/Documentação pública, captura de tela, estudo de caso sanitizado
test/Testes de unidade e integração

Licença

Apache-2.0