{"_id":"@bnomei/emdash-akari","_rev":"3-ddfc3fe9e00b387c79a03fe7633ebf7a","name":"@bnomei/emdash-akari","dist-tags":{"latest":"0.1.3"},"versions":{"0.1.0":{"name":"@bnomei/emdash-akari","version":"0.1.0","keywords":["astro","cloudflare","cms","content-discovery","d1","emdash","emdash-plugin","fts","search","typescript"],"author":{"url":"https://bnomei.com","name":"Bruno Meilick","email":"b@bnomei.com"},"license":"MIT","_id":"@bnomei/emdash-akari@0.1.0","maintainers":[{"name":"bnomei","email":"b@bnomei.com"}],"homepage":"https://github.com/bnomei/emdash-akari#readme","bugs":{"url":"https://github.com/bnomei/emdash-akari/issues"},"bin":{"akari":"dist/cli.mjs"},"dist":{"shasum":"5313a215215f3976a33b55aec3c91f9899f97f96","tarball":"https://registry.npmjs.org/@bnomei/emdash-akari/-/emdash-akari-0.1.0.tgz","fileCount":10,"integrity":"sha512-/JWkC/gYUrzfVx3BW5p4dbH4sMARp/GblrZzVCjkkbUeLffAjy4Ckn0g8R5uD+xfFtHfagNdqRvk5H8eIXWzSQ==","signatures":[{"sig":"MEYCIQCmP8wonKS9TLcExUKMVJu7kcD3vZvhwEpU7amSu0q65AIhAM4kcHzdeJOKC5cLtVQ9OLrWztj9fLvIVXZ556vvbUfv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":93038},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","engines":{"node":">=22.13.0"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"},"./cli":{"types":"./dist/cli.d.mts","import":"./dist/cli.mjs"},"./admin":{"types":"./dist/admin.d.mts","import":"./dist/admin.mjs"}},"gitHead":"46f7cd92774a133fcc4819afafd9fe530c7f7f1f","scripts":{"test":"npm run build && node --test test/*.test.mjs","build":"vp pack src/index.ts src/admin.tsx src/cli.ts --format esm --dts --clean --tsconfig tsconfig.json","check":"vp check .","prepack":"npm run build","typecheck":"tsc --noEmit","pack:check":"vp pack src/index.ts src/admin.tsx src/cli.ts --format esm --dts --clean --tsconfig tsconfig.json --publint","types:check":"attw --pack . --profile esm-only --no-summary","prepublishOnly":"npm run check && npm run typecheck && npm run test && npm run pack:check && npm run types:check"},"_npmUser":{"name":"bnomei","email":"b@bnomei.com"},"repository":{"url":"git+https://github.com/bnomei/emdash-akari.git","type":"git"},"_npmVersion":"11.16.0","description":"Agent-focused EmDash discovery CLI: resolve exact content targets and query nested JSON beyond MCP search.","directories":{},"sideEffects":false,"_nodeVersion":"26.3.0","dependencies":{"zod":"^4.4.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19.2.7","emdash":"^0.19.0","publint":"^0.3.21","react-dom":"^19.2.7","vite-plus":"^0.1.24","typescript":"^6.0.3","@types/node":"^25.9.3","@types/react":"^19.2.17","@cloudflare/kumo":"^2.5.2","@types/react-dom":"^19.2.3","@arethetypeswrong/cli":"^0.18.3","@phosphor-icons/react":"^2.1.10"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","emdash":">=0.19.0","react-dom":"^18.0.0 || ^19.0.0","@cloudflare/kumo":"^2.5.0","@phosphor-icons/react":"^2.1.10"},"_npmOperationalInternal":{"tmp":"tmp/emdash-akari_0.1.0_1781532055456_0.6117195519250509","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bnomei/emdash-akari","version":"0.1.1","keywords":["astro","cloudflare","cms","content-discovery","d1","emdash","emdash-plugin","fts","search","typescript"],"author":{"url":"https://bnomei.com","name":"Bruno Meilick","email":"b@bnomei.com"},"license":"MIT","_id":"@bnomei/emdash-akari@0.1.1","maintainers":[{"name":"bnomei","email":"b@bnomei.com"}],"homepage":"https://github.com/bnomei/emdash-akari#readme","bugs":{"url":"https://github.com/bnomei/emdash-akari/issues"},"bin":{"akari":"dist/cli.mjs"},"dist":{"shasum":"aa092f6a735bbc8104d74e20804cd2361cfa904e","tarball":"https://registry.npmjs.org/@bnomei/emdash-akari/-/emdash-akari-0.1.1.tgz","fileCount":10,"integrity":"sha512-+dRL56KzP5OoVzdhwNhkAlqRQvUZd9lMbx0jDQMyOMhua7ctDFVOFeVTu1mJC8nFlbN/7aOIU2+bzspopbVuog==","signatures":[{"sig":"MEUCIEfXZrvMxMv3n2BhE5/2nU3IwNHPuMmKefr1Gb/yC9PgAiEAhxl/738T2uz4+s9EIzK+4pyRWatu1MrPcYumXuHzBYA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":98792},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","engines":{"node":">=22.13.0"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"},"./cli":{"types":"./dist/cli.d.mts","import":"./dist/cli.mjs"},"./admin":{"types":"./dist/admin.d.mts","import":"./dist/admin.mjs"}},"gitHead":"265af0960d7c86e735d352bfcf61ada94fda13a1","scripts":{"test":"npm run build && node --test test/*.test.mjs","build":"vp pack src/index.ts src/admin.tsx src/cli.ts --format esm --dts --clean --tsconfig tsconfig.json","check":"vp check .","prepack":"npm run build","typecheck":"tsc --noEmit","pack:check":"vp pack src/index.ts src/admin.tsx src/cli.ts --format esm --dts --clean --tsconfig tsconfig.json --publint","types:check":"attw --pack . --profile esm-only --no-summary","test:coverage":"npm run build && node --experimental-test-coverage \"--test-coverage-include=dist/*.mjs\" --test-coverage-lines=60 --test-coverage-branches=60 --test-coverage-functions=55 --test test/*.test.mjs","prepublishOnly":"npm run check && npm run typecheck && npm run test && npm run pack:check && npm run types:check"},"_npmUser":{"name":"bnomei","email":"b@bnomei.com"},"repository":{"url":"git+https://github.com/bnomei/emdash-akari.git","type":"git"},"_npmVersion":"11.16.0","description":"Agent-focused EmDash discovery CLI: resolve exact content targets and query nested JSON beyond MCP search.","directories":{},"sideEffects":false,"_nodeVersion":"26.3.0","dependencies":{"zod":"^4.4.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"npm@11.4.2","devDependencies":{"react":"^19.2.7","emdash":"^0.19.0","publint":"^0.3.21","react-dom":"^19.2.7","vite-plus":"^0.1.24","typescript":"^6.0.3","@types/node":"^25.9.3","@types/react":"^19.2.17","@cloudflare/kumo":"^2.5.2","@types/react-dom":"^19.2.3","@arethetypeswrong/cli":"^0.18.3","@phosphor-icons/react":"^2.1.10"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","emdash":">=0.19.0","react-dom":"^18.0.0 || ^19.0.0","@cloudflare/kumo":"^2.5.0","@phosphor-icons/react":"^2.1.10"},"_npmOperationalInternal":{"tmp":"tmp/emdash-akari_0.1.1_1781803166679_0.8995545168992141","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@bnomei/emdash-akari","version":"0.1.3","description":"Agent-focused EmDash discovery CLI: resolve exact content targets and query nested JSON beyond MCP search.","keywords":["astro","cloudflare","cms","content-discovery","d1","emdash","emdash-plugin","fts","search","typescript"],"homepage":"https://github.com/bnomei/emdash-akari#readme","bugs":{"url":"https://github.com/bnomei/emdash-akari/issues"},"license":"MIT","author":{"name":"Bruno Meilick","email":"b@bnomei.com","url":"https://bnomei.com"},"repository":{"type":"git","url":"git+https://github.com/bnomei/emdash-akari.git"},"bin":{"akari":"dist/cli.mjs"},"type":"module","sideEffects":false,"main":"./dist/index.mjs","types":"./dist/index.d.mts","exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"},"./admin":{"types":"./dist/admin.d.mts","import":"./dist/admin.mjs"},"./cli":{"types":"./dist/cli.d.mts","import":"./dist/cli.mjs"}},"publishConfig":{"access":"public"},"scripts":{"build":"vp pack src/index.ts src/admin.tsx src/cli.ts --format esm --dts --clean --tsconfig tsconfig.json","check":"vp check .","pack:check":"vp pack src/index.ts src/admin.tsx src/cli.ts --format esm --dts --clean --tsconfig tsconfig.json --publint","prepack":"npm run build","prepublishOnly":"npm run check && npm run typecheck && npm run test && npm run pack:check && npm run types:check","test":"npm run build && node --test test/*.test.mjs","types:check":"attw --pack . --profile esm-only --no-summary","typecheck":"tsc --noEmit","test:coverage":"npm run build && node --experimental-test-coverage \"--test-coverage-include=dist/*.mjs\" --test-coverage-lines=60 --test-coverage-branches=60 --test-coverage-functions=55 --test test/*.test.mjs"},"dependencies":{"zod":"^4.4.1"},"devDependencies":{"@arethetypeswrong/cli":"^0.18.3","@cloudflare/kumo":"^2.5.2","@phosphor-icons/react":"^2.1.10","@types/node":"^25.9.3","@types/react":"^19.2.17","@types/react-dom":"^19.2.3","emdash":"^0.19.0","publint":"^0.3.21","react":"^19.2.7","react-dom":"^19.2.7","typescript":"^6.0.3","vite-plus":"^0.1.24"},"peerDependencies":{"@cloudflare/kumo":"^2.5.0","@phosphor-icons/react":"^2.1.10","emdash":">=0.19.0","react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"engines":{"node":">=22.13.0"},"packageManager":"npm@11.4.2","gitHead":"235d30d80cc0208f6db2548d32aeb7a3439ff128","_id":"@bnomei/emdash-akari@0.1.3","_nodeVersion":"26.4.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-RXlQSJFfC+ptupgnAfxg0oL/Ma9xqoBmAe8j5/fW+IXFt/rA+Fs8dzotuyxmOTvpFkM1fkobkJsitBfFjEqsQA==","shasum":"aa365eb728461ee39a4e2000195235e810d69f73","tarball":"https://registry.npmjs.org/@bnomei/emdash-akari/-/emdash-akari-0.1.3.tgz","fileCount":10,"unpackedSize":119230,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD+hxprXn7fHzNoZKgrKAjfsateTHQMzaEON0QtNkp3CgIgSTzVk8wzb2S5aDL4E0kUNG/mnjyy30z1GyDem+QisbY="}]},"_npmUser":{"name":"bnomei","email":"b@bnomei.com"},"directories":{},"maintainers":[{"name":"bnomei","email":"b@bnomei.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/emdash-akari_0.1.3_1782733029696_0.14800986649522585"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-15T14:00:55.202Z","modified":"2026-06-29T11:37:09.998Z","0.1.0":"2026-06-15T14:00:55.621Z","0.1.1":"2026-06-18T17:19:26.812Z","0.1.3":"2026-06-29T11:37:09.885Z"},"bugs":{"url":"https://github.com/bnomei/emdash-akari/issues"},"author":{"name":"Bruno Meilick","email":"b@bnomei.com","url":"https://bnomei.com"},"license":"MIT","homepage":"https://github.com/bnomei/emdash-akari#readme","keywords":["astro","cloudflare","cms","content-discovery","d1","emdash","emdash-plugin","fts","search","typescript"],"repository":{"type":"git","url":"git+https://github.com/bnomei/emdash-akari.git"},"description":"Agent-focused EmDash discovery CLI: resolve exact content targets and query nested JSON beyond MCP search.","maintainers":[{"name":"bnomei","email":"b@bnomei.com"}],"readme":"# @bnomei/emdash-akari\n\n[![npm version](https://img.shields.io/npm/v/@bnomei/emdash-akari.svg)](https://www.npmjs.com/package/@bnomei/emdash-akari)\n[![npm downloads](https://img.shields.io/npm/dm/@bnomei/emdash-akari.svg)](https://www.npmjs.com/package/@bnomei/emdash-akari)\n[![license](https://img.shields.io/npm/l/@bnomei/emdash-akari.svg)](https://www.npmjs.com/package/@bnomei/emdash-akari)\n[![types](https://img.shields.io/badge/types-included-blue.svg)](./package.json)\n[![source](https://img.shields.io/badge/source-GitHub-181717.svg?logo=github)](https://github.com/bnomei/emdash-akari)\n\nPrivate content discovery and identity resolution for EmDash.\n\nAkari means light. In EmDash, Akari is the private lookup surface for finding\nthe canonical content entry behind a query, nested content condition, dashboard\ntask, or agent task. It is not the public site search endpoint and it does not\nown a separate search infrastructure.\n\nAkari's primary surface is the `akari` binary. The package also registers\nprivate EmDash plugin routes because the binary needs a protected way to talk to\nthe EmDash app, but direct HTTP integration is not the main use case.\n\nAkari complements the EmDash\n[MCP server](https://docs.emdashcms.com/reference/mcp-server/). The MCP server\nis a broad content administration surface with tools such as `search`,\n`content_list`, `content_get`, schema, media, taxonomy, menu, revision, and\nsettings tools. Akari is narrower: it answers lookup and identity questions in\none request, especially when the answer depends on nested JSON structure,\nevidence paths, or whether a match is clear enough to automate.\n\nAkari is useful when you need to:\n\n- find the canonical content entry for an intent before editing it,\n- check whether a topic already exists before an agent creates another page,\n- find entries that contain a block type, embed, external URL, or nested JSON\n  value,\n- inspect which content would be affected by a schema, layout, or block\n  migration,\n- resolve one target only when the top match is not ambiguous.\n\n## Quick Start\n\nInstall the package:\n\n```sh\nnpm install @bnomei/emdash-akari\n```\n\nRegister the native plugin in `astro.config.mjs`:\n\n```js\nimport emdash from \"emdash/astro\";\nimport { akariPlugin } from \"@bnomei/emdash-akari\";\n\nexport default {\n  integrations: [\n    emdash({\n      plugins: [akariPlugin()],\n    }),\n  ],\n};\n```\n\nThe normal connection path is:\n\n1. Register `akariPlugin()` in the EmDash Astro app.\n2. Run the EmDash dev server.\n3. Set the EmDash app URL and token for the binary.\n4. Smoke-test the connection with `akari config`.\n5. Use `akari discover` or `akari resolve`.\n6. Optionally wrap the same binary in a local MCP tool.\n\nSet the EmDash app root and token:\n\n```sh\nexport EMDASH_BASE_URL=http://localhost:4321\nexport EMDASH_TOKEN=\"...\"\n```\n\n`EMDASH_BASE_URL` is the Astro/EmDash app root. The CLI appends the plugin path\nitself. `EMDASH_TOKEN` should be an EmDash PAT or OAuth access token with the\n`admin` scope, issued to an Admin user.\n\nSmoke-test the private route from a consuming app:\n\n```sh\nnpm exec -- akari config --pretty\n```\n\nDiscover candidates:\n\n```sh\nnpm exec -- akari discover --pretty --data '{\n  \"q\": \"Workers AI inference guide\",\n  \"collections\": [\"pages\", \"products\"],\n  \"filter\": { \"status\": \"published\" },\n  \"limit\": 10\n}'\n```\n\nResolve one target before an automated edit:\n\n```sh\nnpm exec -- akari resolve --pretty --data '{\n  \"q\": \"main D1 product guide\",\n  \"collections\": [\"products\"],\n  \"filter\": { \"status\": \"published\" },\n  \"maxAlternatives\": 3\n}'\n```\n\n`discover` returns ranked candidates. `resolve` returns one identity when the\ntop candidate is clear enough. `config` returns the route capabilities.\n\n## Akari vs MCP\n\nEmDash MCP already has a `search` tool for indexed full-text search. That is the\nright surface when an agent wants ordinary search results. Akari is for richer\nlookup questions where the caller needs an identity, evidence, or structural\nfiltering without chaining several MCP calls and doing client-side inspection.\n\nMCP search returns ordinary indexed hits:\n\n```json\n{\n  \"items\": [\n    {\n      \"collection\": \"pages\",\n      \"id\": \"page_workers_ai\",\n      \"slug\": \"workers-ai\",\n      \"locale\": \"en\",\n      \"title\": \"Workers AI\",\n      \"snippet\": \"Build and deploy <mark>Workers AI</mark> inference...\",\n      \"score\": 0.5\n    }\n  ]\n}\n```\n\nThat is enough for search. The MCP score is the raw EmDash/FTS relevance score\nfor that search call. Akari keeps the lookup private and adds identity\nresolution, structural path filters, facets, and ambiguity handling. Its score\nis normalized per response after rank fusion, so it is useful for ordering and\nambiguity checks, not for numeric comparison with MCP search.\n\nFind the likely canonical entry for a topic:\n\n```sh\nnpm exec -- akari discover --pretty --data '{\n  \"q\": \"Workers AI inference guide\",\n  \"collections\": [\"pages\", \"products\"],\n  \"filter\": { \"status\": \"published\" },\n  \"select\": [\"identity\", \"score\", \"snippet\"],\n  \"limit\": 3\n}'\n```\n\nShape of the answer:\n\n```json\n{\n  \"items\": [\n    {\n      \"identity\": {\n        \"collection\": \"pages\",\n        \"id\": \"page_workers_ai\",\n        \"slug\": \"workers-ai\",\n        \"title\": \"Workers AI\"\n      },\n      \"score\": 1,\n      \"snippet\": \"Build and deploy <mark>Workers AI</mark> inference...\"\n    }\n  ]\n}\n```\n\nWith MCP alone, an agent would usually call `search`, inspect results, and often\nfollow up with `content_get` before it knows which entry is safe to edit.\n\nFind nested content structure:\n\n```sh\nnpm exec -- akari discover --pretty --data '{\n  \"mode\": \"structural\",\n  \"collections\": [\"pages\"],\n  \"paths\": [\n    { \"path\": \"$.blocks[*].type\", \"op\": \"eq\", \"value\": \"embed\" },\n    { \"path\": \"$.blocks[*].url\", \"op\": \"contains\", \"value\": \"developers.cloudflare.com\" }\n  ],\n  \"facets\": [\"collection\", \"$.blocks[*].type\"],\n  \"limit\": 10\n}'\n```\n\nShape of the answer:\n\n```json\n{\n  \"items\": [\n    {\n      \"identity\": {\n        \"collection\": \"pages\",\n        \"id\": \"page_developer_platform\",\n        \"title\": \"Developer Platform\"\n      },\n      \"matchedPaths\": [\"$.blocks[3].type\", \"$.blocks[3].url\"]\n    }\n  ],\n  \"facets\": [{ \"key\": \"$.blocks[*].type\", \"buckets\": [{ \"value\": \"embed\", \"count\": 1 }] }]\n}\n```\n\nWith MCP alone, this kind of question requires listing or searching candidates,\nfetching their full content, walking nested block JSON, keeping track of the\nmatching paths, and then grouping the evidence manually.\n\n## Command Input\n\n`discover` returns ranked candidates, snippets, facets, and evidence.\n\n`resolve` accepts the same search input without facets and returns one stable\nidentity, an ambiguous result, or a missing result.\n\nThe validated query shape is:\n\n```json\n{\n  \"q\": \"Workers AI inference guide\",\n  \"mode\": \"lexical\",\n  \"collections\": [\"pages\", \"products\", \"posts\"],\n  \"filter\": {\n    \"status\": \"published\",\n    \"locale\": \"en\"\n  },\n  \"paths\": [\n    { \"path\": \"$.blocks[*].type\", \"op\": \"eq\", \"value\": \"embed\" },\n    { \"path\": \"$.blocks[*].url\", \"op\": \"exists\" }\n  ],\n  \"select\": [\"identity\", \"title\", \"url\", \"score\", \"snippet\", \"matchedPaths\"],\n  \"facets\": [\"collection\", \"status\", \"$.blocks[*].type\"],\n  \"sort\": [\"-score\", \"-updatedAt\"],\n  \"limit\": 20,\n  \"after\": null,\n  \"explain\": false\n}\n```\n\n`resolve` adds:\n\n```json\n{\n  \"maxAlternatives\": 3\n}\n```\n\nSupported modes:\n\n- `lexical`: full-text search through EmDash's `_emdash_fts_*` tables.\n- `structural`: nested JSON/path search through Akari `paths`.\n\nThe schemas reject unknown keys, invalid operators, invalid JSON paths, empty\ncollection names, out-of-range limits, and unsupported filter value shapes\nbefore the engine receives the request.\n\nUse top-level `collections` as the normal collection selector. If `collections`\nis omitted, Akari can fall back to `filter.collection`; otherwise `filter` is\nbest reserved for metadata such as `status`, `locale`, or `updatedAt`.\n\nCursor pagination (`after` / `nextCursor`) is supported only for single-layer\nlexical queries. When Akari fuses the lexical and content-scan layers (the\ndefault whenever content access is available), the merged ranking has no single\ncontinuation token, so `nextCursor` is omitted. To paginate, run a lexical-only\nquery (no content scan), pass the returned `nextCursor` back as `after`, and\ncontinue until `nextCursor` is absent.\n\nLexical mode does not introduce a second content index. Akari plans against the\nsame EmDash full-text table convention and uses SQLite\n[FTS5](https://sqlite.org/fts5.html) ranking/snippets so `discover` can return\nan identity-shaped answer instead of only a public search hit.\n\nThe exported `buildEmDashFts5Plan` helper omits the status predicate when\n`status` is not provided, so diagnostics and admin tooling can inspect every\nstored status. Pass `status: \"published\"` or another explicit status when the\nplan should constrain rows.\n\nLexical queries are normalized before they reach FTS5:\n\n- Leading and trailing whitespace is ignored.\n- An empty or whitespace-only query is not executable and produces no FTS plan.\n- Plain terms are split on whitespace, quoted, and treated as prefix terms. For\n  example, `workers ai` becomes `\"workers\"* \"ai\"*`, matching words that start\n  with `workers` and `ai`.\n- A fully quoted query stays a phrase query. Internal double quotes are escaped,\n  so `\"workers ai\"` remains a phrase search instead of becoming prefix terms.\n- Queries containing explicit FTS boolean/proximity operators (`AND`, `OR`,\n  `NOT`, or `NEAR`) are passed through as operator queries after double quotes\n  are escaped. For example, `workers OR d1` keeps the `OR` operator.\n\nExamples:\n\n```json\n{ \"q\": \"workers ai\", \"mode\": \"lexical\", \"collections\": [\"pages\"] }\n```\n\nSearches for prefix terms in the configured EmDash FTS table.\n\n```json\n{ \"q\": \"workers OR d1\", \"mode\": \"lexical\", \"collections\": [\"pages\"] }\n```\n\nUses FTS5 boolean semantics for the operator query.\n\nAkari normalizes `score` within each response after rank fusion. Treat it as a\nrelative ordering signal for that result set, not as a probability and not as a\nnumber that can be compared with raw EmDash search scores from MCP.\n\n## Filter Syntax\n\n`filter` is intentionally a small metadata filter subset:\n\n```json\n{\n  \"locale\": { \"$in\": [\"en\", \"de\"] },\n  \"status\": \"published\",\n  \"updatedAt\": { \"$gte\": \"2026-01-01\" }\n}\n```\n\nSupported metadata operators:\n\n- `$eq`, `$ne`\n- `$in`, `$nin`\n- `$lt`, `$lte`, `$gt`, `$gte`\n\nRange operators accept strings or numbers only. Set operators require arrays.\nThe syntax borrows common API filter conventions, but it is deliberately not a\nMongoDB clone: no logical nesting, no regular expressions, and no arbitrary\nquery operators.\n\n## Path Syntax\n\n`paths` uses Akari JSON-path syntax for content-shape questions:\n\n```json\n[\n  { \"path\": \"$.blocks[*].type\", \"op\": \"eq\", \"value\": \"embed\" },\n  { \"path\": \"$.blocks[*].url\", \"op\": \"exists\" }\n]\n```\n\nThis is the part that lets a caller ask \"which pages contain an embed block with\nan external URL?\" without fetching every candidate entry and walking block JSON\nclient-side.\n\nWildcard paths are Akari syntax, not raw D1 JSON paths. Direct scalar paths can\ncompile to `json_extract`; wildcard paths compile to `json_each` joins or can be\nserved from sidecar facts. That keeps the public query shape stable while the\nengine uses the SQLite JSON functions available in\n[Cloudflare D1](https://developers.cloudflare.com/d1/sql-api/query-json/) and\n[SQLite JSON1](https://sqlite.org/json1.html).\n\nSupported path operators:\n\n- `exists`\n- `eq`, `ne`\n- `in`, `nin`\n- `contains`, `match`\n- `lt`, `lte`, `gt`, `gte`\n\nPaths may contain `[*]` wildcards. The `discover`/`resolve` engine, materialized\nfacts, and exported structural SQL compiler (`compileStructuralFilters`) support\nmultiple wildcards in one path (for example, `$.a[*].b[*]`). The SQL compiler\nuses an outer `json_each` join for the first wildcard and nested array-guarded\n`json_each` joins for deeper wildcards.\n\n`ne`/`nin` only match scalar values. `contains` is a substring test for strings\nand an element-membership test for arrays. `match` is a case-insensitive literal\nsubstring over string values (any `%`/`_` are literal, not wildcards). String\nrange comparisons (`lt`/`lte`/`gt`/`gte`) use codepoint ordering — the same\nordering SQLite applies under its default `BINARY` collation — so results are\nidentical whether a filter is evaluated in JS (`discover`) or compiled to D1\nSQLite. These semantics are intentionally aligned across both backends.\n\nFor paths that are queried often, Akari exports facts helpers:\n\n```ts\nimport {\n  AKARI_FACTS_INDEX_SQL,\n  AKARI_FACTS_TABLE_SQL,\n  buildReplaceFactsStatements,\n  buildReplaceFactsStatementsFromExtraction,\n  extractContentFacts,\n} from \"@bnomei/emdash-akari\";\n```\n\nThose helpers materialize configured structural paths into\n`_emdash_content_facts`. The table keeps both `path_template` values such as\n`$.blocks[*].type` for grouping and concrete `full_path` values such as\n`$.blocks[3].type` for evidence.\n\nPrefer `buildReplaceFactsStatementsFromExtraction(options)` when re-indexing an\nentry: it extracts facts and derives the replacement scope from the same\noptions, so it still emits a clearing DELETE when extraction returns zero facts\n(for example after content changes so no configured path matches). Calling\n`buildReplaceFactsStatements(facts)` with an empty `facts` array and no `target`\nis a no-op, because the entry scope cannot be derived from zero rows — pass a\n`target` (or use the from-extraction helper) to clear stale rows.\n\n## Response Shapes\n\nCandidate response:\n\n```json\n{\n  \"items\": [\n    {\n      \"identity\": {\n        \"collection\": \"products\",\n        \"id\": \"product_d1\",\n        \"slug\": \"d1\",\n        \"status\": \"published\",\n        \"title\": \"D1\",\n        \"url\": \"/products/d1\"\n      },\n      \"score\": 1,\n      \"snippet\": \"A page about <mark>D1</mark> serverless SQL.\",\n      \"matchedFields\": [\"title\", \"content\"],\n      \"matchedPaths\": []\n    }\n  ],\n  \"facets\": [\n    {\n      \"field\": \"collection\",\n      \"buckets\": [{ \"value\": \"products\", \"count\": 1 }]\n    }\n  ]\n}\n```\n\nResolved response:\n\n```json\n{\n  \"status\": \"resolved\",\n  \"item\": {\n    \"identity\": {\n      \"collection\": \"products\",\n      \"id\": \"product_d1\",\n      \"slug\": \"d1\",\n      \"status\": \"published\",\n      \"title\": \"D1\",\n      \"url\": \"/products/d1\"\n    },\n    \"score\": 1,\n    \"matchedFields\": [\"title\", \"content\"],\n    \"matchedPaths\": []\n  },\n  \"alternatives\": []\n}\n```\n\nAmbiguous response:\n\n```json\n{\n  \"status\": \"ambiguous\",\n  \"alternatives\": [\n    {\n      \"identity\": {\n        \"collection\": \"products\",\n        \"id\": \"product_d1\",\n        \"title\": \"D1\"\n      },\n      \"score\": 1\n    },\n    {\n      \"identity\": {\n        \"collection\": \"pages\",\n        \"id\": \"page_workers-ai\",\n        \"title\": \"Workers AI\"\n      },\n      \"score\": 0.984\n    }\n  ],\n  \"warnings\": [\"Top candidates are too close to resolve automatically.\"]\n}\n```\n\nMissing response:\n\n```json\n{\n  \"status\": \"not_found\",\n  \"alternatives\": [],\n  \"warnings\": [\"No candidate matched the requested identity constraints.\"]\n}\n```\n\n## Local Confidence\n\nThe package includes a local test setup that does not require D1 or Cloudflare\ncredentials:\n\n```sh\nnpm test\n```\n\nThe test suite builds the package and then runs Node's test runner against:\n\n- the native EmDash plugin descriptor and private route surface,\n- package loading for the `./admin` subpath with and without the export,\n- route input schemas, normalization, and syntax guards,\n- lexical/content rank fusion and resolve ambiguity,\n- private content fallback for structural discovery,\n- structural SQL compilation against local SQLite JSON data,\n- facts extraction and facts replacement SQL planning,\n- local CLI requests against a fake EmDash plugin route,\n- SQLite FTS5 ranking/snippets/prefix search/`fts5vocab`,\n- SQLite JSON1 nested block lookup with `json_each` and `json_extract`.\n\nThis gives a fast feedback loop for the same FTS and JSON primitives Akari uses\nin D1-backed EmDash apps.\n\n## Local and D1 Expectations\n\nAkari is designed to run inside an EmDash app that may use Cloudflare D1 in\nproduction, but the package itself does not open a D1 binding, create a second\nsearch service, or require Cloudflare credentials. The private plugin routes use\nthe EmDash content and search surfaces that the host app already exposes. In a\nD1-backed app, Akari assumes the app keeps the normal EmDash content tables and\n`_emdash_fts_*` FTS tables in sync and that D1 provides the same SQLite FTS5 and\nJSON functions documented for those tables.\n\nLocal development has a narrower boundary:\n\n- `npm test` uses in-memory local SQLite to smoke-test snippets, prefix queries,\n  `json_extract`, and `json_each`; FTS5-only smoke tests run when the local Node\n  SQLite build provides the FTS5 extension. The suite does not contact\n  Cloudflare D1.\n- Local smoke coverage validates Akari's generated SQL and JSON-path behavior\n  against SQLite primitives that D1 also supports, but it is not a replacement\n  for running the registered plugin in your deployed EmDash environment.\n- The CLI talks to the configured EmDash app over `EMDASH_BASE_URL`; without a\n  running app and an admin token it cannot discover or resolve real content.\n- Structural discovery can scan private content through EmDash when content\n  access is available. For frequently queried nested paths, use the exported\n  facts helpers to materialize `_emdash_content_facts`; Akari does not maintain\n  that sidecar table automatically.\n\nSearch and storage behavior therefore differs by environment:\n\n| Environment          | Search/storage source                                                                                       | Important limits                                                                                                                                       |\n| -------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| Local tests          | In-memory SQLite fixtures plus fake plugin routes                                                           | Self-contained smoke coverage only; no D1 latency, auth, migration, or deployment behavior is exercised.                                               |\n| Local EmDash app     | The app's local EmDash storage and private plugin route                                                     | Results reflect local fixtures/content and the configured token; production D1 data is not queried unless the app is connected to it.                  |\n| D1-backed EmDash app | EmDash content tables, `_emdash_fts_*` tables, SQLite JSON functions, and optional Akari facts tables in D1 | Akari assumes EmDash owns schema/migrations/index freshness; D1-specific quotas, consistency, and deployment issues must be validated in the host app. |\n\nIf D1-like confidence is required before adoption, run the self-contained test\nsuite first, then smoke-test `akari config`, `akari discover`, and `akari\nresolve` against the target EmDash app so authentication, table shape, FTS\nfreshness, JSON-path behavior, and content permissions are checked together.\n\nThe empty `./admin` export is intentionally retained. Representative package\nloading imports `@bnomei/emdash-akari/admin` successfully while the export is\npresent, and the same package fixture fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`\nwhen the `./admin` entry is removed.\n\n## Private Routes\n\nAkari exposes private plugin routes because the binary needs a protected bridge\ninto the EmDash app:\n\n```txt\n/_emdash/api/plugins/akari/discover\n/_emdash/api/plugins/akari/resolve\n/_emdash/api/plugins/akari/config\n```\n\nThese routes are not the primary integration surface. Prefer the `akari` binary\nfor scripts, agents, and MCP wrappers.\n\nDo not expose the routes through visitor-facing browser code, public pages,\npublic search UI, unauthenticated API proxies, or public MCP servers. Private\nplugin routes run behind EmDash plugin/admin route authentication, so Akari can\nsupport admin diagnostics, draft-aware lookup, agent workflows, and richer\nprojections without turning every lookup into a public data exposure problem.\n\nDashboard/session calls must be same-origin, authenticated as an EmDash user\nwith plugin permissions, and include `X-EmDash-Request: 1` on POST requests.\nServer-side, CLI, agent, and MCP calls should use\n`Authorization: Bearer <token>` with an EmDash PAT or OAuth access token that\nhas the `admin` scope and belongs to an Admin user.\n\n`X-EmDash-Request` is CSRF protection for session-authenticated POST requests.\nIt is not authentication.\n\nPublic site search should stay on EmDash's existing public search endpoint.\n\nIf a process cannot shell out, it can import the same thin route callers:\n\n```ts\nimport { discoverAkari, resolveAkari } from \"@bnomei/emdash-akari/cli\";\n```\n\n## License\n\nMIT.\n\n## Coverage\n\nCI runs the existing Node test suite with the built-in test coverage reporter:\n\n```sh\nnpm run test:coverage\n```\n\nThe coverage gate is intentionally maintainable and low-noise: it only includes\nbuilt package files in `dist/*.mjs` and currently requires at least 60% line\ncoverage, 60% branch coverage, and 55% function coverage. Those thresholds are\nset in the `test:coverage` script in `package.json` so the local command and CI\nuse the same expectations.\n\nWhen coverage changes intentionally, update the threshold values in\n`package.json` in the same pull request as the related test or implementation\nchange. Prefer raising thresholds after adding meaningful tests; lower them only\nwhen the uncovered code is intentionally difficult to exercise and note the\nreason in the pull request.\n","readmeFilename":"README.md"}