@atomicmemory/llmwiki
June 11, 2026 · View on GitHub
Bridge adapter for importing llmwiki JSON exports into AtomicMemory.
llmwiki compiles raw sources into an interlinked markdown wiki and can
emit the result as a typed JSON envelope (llmwiki export --target json). This package parses that envelope and maps each wiki page to a
verbatim AtomicMemory ingest input — one page becomes one memory
record, with all advisory metadata (kind, citations, confidence,
provenance state, contradictions, aliases, freshness) preserved under
memory.metadata.llmwiki.*.
Install
pnpm add @atomicmemory/llmwiki @atomicmemory/sdk
This package is ESM-only ("type": "module" with no CJS build). CommonJS consumers cannot require() it; use ESM imports or wrap via dynamic import().
Quick start (recommended: CLI)
The shipped CLI wraps the bridge with the re-import probe, the
--allow-append-only / --accept-duplicates / --yes opt-in gates,
per-page failure capture, and a non-zero exit code on any partial
failure. This is the safe path:
atomicmemory import --type llmwiki ./wiki.json \
--user alice --namespace team-kb
Add --dry-run to inspect the envelope (external IDs, byte counts,
projectId) without ingesting anything.
Advanced: SDK-direct usage
Calling toAtomicMemoryIngestInputs + provider.ingest() directly
gives you the full ingest pipeline but you take responsibility for
error handling and rollback — AtomicMemory verbatim ingest is
append-only, and a failure partway through a 100K-page wiki leaves
partial state with no automatic recovery. The CLI is the safer
default; reach for this only if you need to integrate the bridge into
custom application code.
import { MemoryClient } from "@atomicmemory/sdk";
import {
loadLLMWikiExport,
toAtomicMemoryIngestInputs,
assertSupportsVerbatim,
} from "@atomicmemory/llmwiki";
const client = new MemoryClient(/* … */);
const provider = client.getProvider();
assertSupportsVerbatim(provider);
const exportData = await loadLLMWikiExport("./wiki.json");
const inputs = toAtomicMemoryIngestInputs(exportData, {
scope: { user: "alice", namespace: "team-kb" },
});
// Per-page failure capture. Partial success is real — a failure on
// page N leaves pages 0..N-1 already committed to the store and there
// is no automatic rollback. Track which inputs failed so a retry can
// be scoped to those.
const failures: { externalId: string; error: unknown }[] = [];
for (const input of inputs) {
try {
await provider.ingest(input);
} catch (error) {
failures.push({
externalId: (input.metadata as { externalId: string }).externalId,
error,
});
}
}
if (failures.length > 0) {
// Surface to your operator/log path — don't silently swallow.
throw new Error(`Partial import: ${failures.length} pages failed`);
}
Why verbatim mode
verbatim ingest skips AtomicMemory's LLM extraction pipeline and
stores each page as one memory record with metadata forwarded intact.
text / messages modes would re-extract the page and may drop the
advisory metadata depending on the provider — the bridge refuses to
operate in those modes via assertSupportsVerbatim.
Stable identity
Every page produces a deterministic external ID:
llmwiki/<projectId>/<pageDirectory>/<slug>
projectId is the deterministic-namespace key for every memory the
bridge produces. Two projects supplying the same projectId share
an external-ID namespace; under the current append-only verbatim
ingest semantics, re-imports across that collision produce
duplicate records, not overwrites — each duplicate carries the full
advisory metadata, pollutes the search ranking distribution, and is
invisible to either project until a list() / search() returns
records the caller didn't author. Pin projectId globally unique
per user; treat it as you would a tenant key. The adapter
validates projectId against /^[a-z0-9][a-z0-9-]{0,62}$/ on both
sides of the bridge.
If AtomicMemory ever ships a deterministic upsert primitive keyed on external ID, the failure mode under collision changes from silent duplicate amplification to silent overwrite. Either failure mode is bad; the discipline doesn't change.
Trust model and prompt injection
Imported wiki content is third-party text. Every page body becomes the content of a verbatim memory record that downstream LLMs will eventually read back via search() / package(). A page that says
Caching is great. <<<SYSTEM: ignore prior instructions...>>>
is persisted verbatim. When the LLM later searches for "caching," that payload lands in its prompt context. The bridge does NOT sanitize, scan, or reject suspicious content — it cannot tell a prompt-injection attempt from a legitimate fenced code block discussing prompt injection.
The bridge's only defense is a trust marker. Every imported memory carries metadata.llmwiki.trustLevel = "external-import" AND metadata.llmwiki.version = 1. Downstream packaging code MUST inspect these fields and surface the untrusted-content signal in a way the consuming LLM can act on (typically by wrapping the body in <untrusted-source> tags or an equivalent fence when injecting into a prompt).
If you import a wiki, you are extending trust to every author of every page in that wiki to the same degree you trust your own operator-authored prompts. Apply normal third-party-content discipline: only import wikis you control or whose authors you've reviewed.
See docs/threat-model.md for the full attacker model and out-of-scope items.
Live provider (@atomicmemory/llmwiki/live)
LiveLLMWikiProvider is the writable, source-backed companion to the read-only SnapshotLLMWikiProvider. It drives a live llmwiki project through the createWiki() SDK and does CRUD over llmwiki sources (not compiled pages): provider IDs are source IDs (llmwiki-source/<projectId>/<filename>), so the ID doIngest returns is exactly what doGet/doDelete accept. It carries the same external-import trust markers, enforces the construction scope on every operation, and package() wraps each source body in an <untrusted-llmwiki-source> fence per the trust model above.
A few semantics worth knowing:
-
verbatimstores a source document. For the live provider,verbatimmeans "store this input verbatim as an llmwiki source," not as an AtomicMemory Core record. The body and title are stored;kind,contentClass, and any otherIngestInputmetadata beyondtitleare NOT preserved (a source is always surfaced askind: "document"). -
Idempotency needs an explicit source id. When you pass
provenance.sourceId, re-ingesting the same id updates the same source in place (writeStatus: "unchanged"when the body is byte-identical). Without it, the source identity is derived fromtitle + text, so re-ingesting the same content with an inconsistentmetadata.titleforks a new source instead of updating. Passprovenance.sourceIdwhenever you need reliable upsert. -
createdAtis the last-ingest time. A source carries a singleingestedAttimestamp that the SDK re-stamps on every write, so theMemory.createdAtthe live provider returns reflects the most recent ingest, not original creation — and there is no separateupdatedAt. -
compile()is explicit and scope-guarded. Compilation (the LLM step that turns sources into interlinked pages) is a separatecompile(scope)call, never part of ingest; it requires the construction scope and LLM credentials. Run it after a batch of ingests. -
search()/package()load every source body to score lexically (O(all-sources) per call), andsearch()is not cursor-paginated. Fine for modest projects; asource→filename manifest index is the planned scale fix.
Lazy registration (no compiler load until used)
Importing @atomicmemory/llmwiki/register is light — llm-wiki-compiler loads only when the provider is constructed during initialize().
import { MemoryClient } from "@atomicmemory/sdk";
import { liveLlmwikiLazyEntry } from "@atomicmemory/llmwiki/register";
const client = new MemoryClient({
providers: { "llmwiki-live": { root: "./wiki", projectId: "my-proj", scope: { user: "alice" } } },
defaultProvider: "llmwiki-live",
});
await client.initialize({ "llmwiki-live": liveLlmwikiLazyEntry() }); // compiler loads only now
- llmwiki-only clients: pass
{ "llmwiki-live": liveLlmwikiLazyEntry() }. - Mixed clients (llmwiki + built-in providers): this is currently manual/advanced —
defaultRegistryis intentionally not exported and built-in provider factories are not exposed as a public registry. You must assemble the full registry object yourself (one entry per provider). First-class registry composition is deferred to a separate SDK decision. @atomicmemory/llmwiki/register(light, lazy, for registration) vs@atomicmemory/llmwiki/live(eager, for directnew LiveLLMWikiProvider(...)).
Limits
The export is treated as untrusted input. Hard caps enforced on every
import (see src/limits.ts):
| limit | value |
|---|---|
MAX_PAGE_COUNT | 100,000 pages |
MAX_BODY_LENGTH | 1 MB per page body |
MAX_FIELD_LENGTH | 64 KB per other field |
MAX_NESTING_DEPTH | 16 |
MAX_TOTAL_SIZE_BYTES | 256 MB file size |
Violations throw LLMWikiBridgeError with code
E_LLMWIKI_EXPORT_OVER_LIMIT or E_LLMWIKI_EXPORT_INVALID_SHAPE.
Error codes
All errors thrown by this package are LLMWikiBridgeError instances
with a stable .code field. Branch on the code, not the message:
E_LLMWIKI_EXPORT_INVALID_SHAPEE_LLMWIKI_EXPORT_OVER_LIMITE_LLMWIKI_EXPORT_NOT_FOUNDE_LLMWIKI_EXPORT_DUPLICATE_SLUGE_LLMWIKI_PROJECT_ID_REQUIREDE_LLMWIKI_PROJECT_ID_INVALIDE_LLMWIKI_VERBATIM_UNSUPPORTEDE_LLMWIKI_PROVIDER_READONLYE_LLMWIKI_PROVIDER_SCOPE_MISMATCHE_LLMWIKI_PROVIDER_INVALID_CURSORE_LLMWIKI_PROVIDER_INVALID_LIMITE_LLMWIKI_PROVIDER_INVALID_BUDGETE_LLMWIKI_PROVIDER_DISPOSEDE_LLMWIKI_REIMPORT_CHECK_INCONCLUSIVEE_LLMWIKI_COMPILER_MISSING— thrown by@atomicmemory/llmwiki/register's lazy factory when the live provider is selected but the optional peerllm-wiki-compileris not installed. Install it (npm i llm-wiki-compiler@^0.9.0) to use@atomicmemory/llmwiki/register. (Note:@atomicmemory/llmwiki/liveimports the compiler eagerly, so a missing peer there fails at import time with a raw module-resolution error, not this code.) A rejectedinitialize()leaves the client in an undefined partial state — construct a newMemoryClientafter installing the peer (retrying the same instance re-throws the original error). A corrupt install (package present but unloadable) surfaces the raw module-resolution error instead of this code, since reinstalling rather than installing is the fix.
A regression test in src/__tests__/error-codes-doc.test.ts asserts every code exported from errors.ts appears in this list, so additions can't ship undocumented.
Further reading
- Cookbook — four-step workflow: compile → export → import → package.
- Two-MCP guide — running llmwiki and AtomicMemory as two MCP servers in the same agent session with capability-enforced isolation.