renor-engine 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +73 -0
  3. package/README.md +105 -2
  4. package/dist/BUILD.json +6 -0
  5. package/dist/api/apiAuth.js +43 -0
  6. package/dist/api/server.js +172 -0
  7. package/dist/buildIdentity.js +132 -0
  8. package/dist/cli-extractors.js +46 -0
  9. package/dist/cli.js +491 -0
  10. package/dist/config.js +65 -0
  11. package/dist/context/composer.js +320 -0
  12. package/dist/context/readHonesty.js +478 -0
  13. package/dist/context/rowStore.js +238 -0
  14. package/dist/conversation/formatter.js +81 -0
  15. package/dist/conversation/parser.js +189 -0
  16. package/dist/conversation/types.js +2 -0
  17. package/dist/conversation/writer.js +42 -0
  18. package/dist/db/client.js +147 -0
  19. package/dist/db/indexSalvage.js +81 -0
  20. package/dist/db/migrations.js +642 -0
  21. package/dist/db/readCache.js +109 -0
  22. package/dist/db/schema.js +5 -0
  23. package/dist/db/statements.js +89 -0
  24. package/dist/digests/digestCache.js +142 -0
  25. package/dist/digests/digests.js +597 -0
  26. package/dist/digests/worker.js +348 -0
  27. package/dist/embeddings/caching.js +337 -0
  28. package/dist/embeddings/coalescing.js +177 -0
  29. package/dist/embeddings/interactive.js +103 -0
  30. package/dist/embeddings/local.js +68 -0
  31. package/dist/embeddings/openai.js +105 -0
  32. package/dist/embeddings/rebuild.js +166 -0
  33. package/dist/embeddings/simdScan.js +258 -0
  34. package/dist/embeddings/space.js +21 -0
  35. package/dist/embeddings/types.js +14 -0
  36. package/dist/embeddings/vectorIndex.js +675 -0
  37. package/dist/embeddings/vertex.js +224 -0
  38. package/dist/eval/artifacts.js +42 -0
  39. package/dist/eval/cacheReport.js +139 -0
  40. package/dist/eval/comparison.js +182 -0
  41. package/dist/eval/context700-candidate.js +145 -0
  42. package/dist/eval/evalCache.js +525 -0
  43. package/dist/eval/hashEmbedding.js +35 -0
  44. package/dist/eval/longmemeval.js +56 -0
  45. package/dist/eval/metrics.js +181 -0
  46. package/dist/eval/renorbenchBaseline.js +486 -0
  47. package/dist/eval/renorbenchDocuments.js +134 -0
  48. package/dist/eval/renorbenchProduct.js +182 -0
  49. package/dist/eval/runLedger.js +279 -0
  50. package/dist/eval/runManifest.js +479 -0
  51. package/dist/eval/scoring.js +197 -0
  52. package/dist/evidence/attach.js +267 -0
  53. package/dist/evidence/background.js +147 -0
  54. package/dist/evidence/evidenceCache.js +182 -0
  55. package/dist/evidence/gate.js +37 -0
  56. package/dist/evidence/keyPool.js +85 -0
  57. package/dist/evidence/ollamaChat.js +84 -0
  58. package/dist/evidence/optin.js +165 -0
  59. package/dist/evidence/options.js +130 -0
  60. package/dist/evidence/secondPass.js +456 -0
  61. package/dist/extraction/classifyCapture.js +34 -0
  62. package/dist/extraction/decision-model.js +174 -0
  63. package/dist/extraction/eventExtractor.js +113 -0
  64. package/dist/extraction/extractionClient.js +122 -0
  65. package/dist/extraction/factExtractor.js +46 -0
  66. package/dist/extraction/factsWritePath.js +179 -0
  67. package/dist/extraction/preferenceExtractor.js +153 -0
  68. package/dist/extraction/secretGuard.js +176 -0
  69. package/dist/extraction/secretScanner.js +665 -0
  70. package/dist/extraction/typedFacts.js +224 -0
  71. package/dist/extraction/writePathAdjudication.js +110 -0
  72. package/dist/importing/apply.js +644 -0
  73. package/dist/importing/bundle-write.js +80 -0
  74. package/dist/importing/cli.js +407 -0
  75. package/dist/importing/existing.js +49 -0
  76. package/dist/importing/facts.js +123 -0
  77. package/dist/importing/forgetImported.js +79 -0
  78. package/dist/importing/hosted.js +467 -0
  79. package/dist/importing/index.js +29 -0
  80. package/dist/importing/journalLog.js +96 -0
  81. package/dist/importing/markdown.js +98 -0
  82. package/dist/importing/plan.js +206 -0
  83. package/dist/importing/secretScreen.js +113 -0
  84. package/dist/importing/serialize.js +188 -0
  85. package/dist/importing/source.js +463 -0
  86. package/dist/importing/types.js +6 -0
  87. package/dist/indexer/chunker.js +319 -0
  88. package/dist/indexer/dailyEntries.js +63 -0
  89. package/dist/indexer/hasher.js +10 -0
  90. package/dist/indexer/laneRuns.js +48 -0
  91. package/dist/indexer/o200k.js +481 -0
  92. package/dist/indexer/o200k.json +8 -0
  93. package/dist/indexer/outsideRoot.js +77 -0
  94. package/dist/indexer/parser.js +167 -0
  95. package/dist/indexer/pipeline.js +1168 -0
  96. package/dist/indexer/staleness.js +201 -0
  97. package/dist/indexer/tokenizer.js +77 -0
  98. package/dist/indexer/walker.js +184 -0
  99. package/dist/mcp/hive.js +88 -0
  100. package/dist/mcp/requestErrors.js +47 -0
  101. package/dist/mcp/server.js +396 -0
  102. package/dist/memory/agentLease.js +43 -0
  103. package/dist/memory/confinedWrite.js +315 -0
  104. package/dist/memory/controls.js +73 -0
  105. package/dist/memory/dailyNote.js +265 -0
  106. package/dist/memory/durableAppend.js +57 -0
  107. package/dist/memory/factConflicts.js +203 -0
  108. package/dist/memory/factTerms.js +321 -0
  109. package/dist/memory/hiveNameHeader.cjs +10 -0
  110. package/dist/memory/inventory.js +55 -0
  111. package/dist/memory/invisibleText.js +20 -0
  112. package/dist/memory/lifecycle.js +1756 -0
  113. package/dist/memory/limits.js +13 -0
  114. package/dist/memory/requests.js +183 -0
  115. package/dist/memory/sourceName.js +200 -0
  116. package/dist/memory/whatsNew.js +158 -0
  117. package/dist/memory/writer.js +175 -0
  118. package/dist/memory/writerConflicts.js +85 -0
  119. package/dist/memorymap/memoryMap.js +1568 -0
  120. package/dist/memorymap/replacedJournal.js +58 -0
  121. package/dist/memorymap/replacements.js +405 -0
  122. package/dist/mvp/activity.js +86 -0
  123. package/dist/mvp/agentMemory.js +1744 -0
  124. package/dist/mvp/auth.js +531 -0
  125. package/dist/mvp/cli.js +73 -0
  126. package/dist/mvp/client-store.js +462 -0
  127. package/dist/mvp/compileCache.js +249 -0
  128. package/dist/mvp/compileSeed.js +27 -0
  129. package/dist/mvp/config.js +288 -0
  130. package/dist/mvp/connectionStatus.js +80 -0
  131. package/dist/mvp/contextRelevance.js +413 -0
  132. package/dist/mvp/engine.js +1065 -0
  133. package/dist/mvp/exportAll.js +359 -0
  134. package/dist/mvp/factLanePack.js +77 -0
  135. package/dist/mvp/factMarkers.js +49 -0
  136. package/dist/mvp/factsLane.js +203 -0
  137. package/dist/mvp/hiddenSpans.js +585 -0
  138. package/dist/mvp/history.js +413 -0
  139. package/dist/mvp/hitDates.js +31 -0
  140. package/dist/mvp/hitFloor.js +36 -0
  141. package/dist/mvp/identity.js +18 -0
  142. package/dist/mvp/idle.js +113 -0
  143. package/dist/mvp/memoryDir.js +98 -0
  144. package/dist/mvp/posthogMcp.js +210 -0
  145. package/dist/mvp/readScope.js +104 -0
  146. package/dist/mvp/readUsefulness.js +222 -0
  147. package/dist/mvp/sanitize.js +48 -0
  148. package/dist/mvp/schemaRejections.js +91 -0
  149. package/dist/mvp/scope.js +127 -0
  150. package/dist/mvp/server.js +2089 -0
  151. package/dist/mvp/serverCard.js +29 -0
  152. package/dist/mvp/storedSize.js +139 -0
  153. package/dist/mvp/telemetry.js +441 -0
  154. package/dist/retrieval/adviceLines.js +195 -0
  155. package/dist/retrieval/adviceQuestion.js +44 -0
  156. package/dist/retrieval/conversationShape.js +44 -0
  157. package/dist/retrieval/dailyEntryTime.js +55 -0
  158. package/dist/retrieval/entityLookup.js +113 -0
  159. package/dist/retrieval/factsFirst.js +188 -0
  160. package/dist/retrieval/keyword.js +84 -0
  161. package/dist/retrieval/ledgerEventTime.js +51 -0
  162. package/dist/retrieval/ledgerLiveness.js +83 -0
  163. package/dist/retrieval/ledgerPresentation.js +161 -0
  164. package/dist/retrieval/noteIdentity.js +57 -0
  165. package/dist/retrieval/orchestrator.js +1086 -0
  166. package/dist/retrieval/queryHelpers.js +142 -0
  167. package/dist/retrieval/questionSpan.js +475 -0
  168. package/dist/retrieval/readerEvents.js +690 -0
  169. package/dist/retrieval/readerLayout.js +503 -0
  170. package/dist/retrieval/relativeTimeHints.js +113 -0
  171. package/dist/retrieval/semantic.js +67 -0
  172. package/dist/router/routerCache.js +106 -0
  173. package/dist/router/sessionRouter.js +726 -0
  174. package/dist/toolRefusal.js +120 -0
  175. package/dist/watcher/watcher.js +75 -0
  176. package/package.json +120 -4
package/NOTICE ADDED
@@ -0,0 +1,73 @@
1
+ Copyright 2026 Patrick Passos
2
+
3
+ Renor's own work in this tree is licensed under the GNU Affero General
4
+ Public License, Version 3 (AGPL-3.0-only). See the LICENSE file.
5
+
6
+ Files imported from the public project obsidian-semantic-memory
7
+ (https://github.com/sterben-enec/obsidian-semantic-memory) stay under
8
+ that project's ISC license. The upstream notice is reproduced below
9
+ unchanged. It is not an AGPL-3.0 grant, and the upstream copyright
10
+ line is not Renor's.
11
+
12
+ -----
13
+
14
+ ISC License
15
+
16
+ Copyright (c) 2026 sterben_enec
17
+
18
+ Permission to use, copy, modify, and/or distribute this software for any
19
+ purpose with or without fee is hereby granted, provided that the above
20
+ copyright notice and this permission notice appear in all copies.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
23
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
24
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
25
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
26
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
27
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
28
+ PERFORMANCE OF THIS SOFTWARE.
29
+
30
+ -----
31
+
32
+ Third-party components
33
+
34
+ @img/sharp-libvips-linux-x64 and the musl build (@img/sharp-libvips-linuxmusl-x64)
35
+ are LGPL-3.0-or-later. @img/sharp-wasm32 is Apache-2.0 AND LGPL-3.0-or-later
36
+ AND MIT. The sharp override in package.json pulls them in so
37
+ @xenova/transformers can run a local embedding model. They ship as separate
38
+ shared objects, not as AGPL-3.0 source. LGPL-3.0 stays compatible with
39
+ this AGPL-3.0-only program while those libraries remain separate and this
40
+ notice is kept. They are not AGPL-3.0.
41
+
42
+ Token vocabulary (src/indexer/o200k.json)
43
+
44
+ The file src/indexer/o200k.json holds the o200k_base token vocabulary packed
45
+ from the copy that the gpt-tokenizer package ships (https://github.com/niieani/gpt-tokenizer),
46
+ by scripts/tokenizer/gen-o200k.cjs. The license of that package is reproduced
47
+ below unchanged.
48
+
49
+ -----
50
+
51
+ MIT License
52
+
53
+ Copyright (c) 2023-2024 Bazyli Brzoska
54
+
55
+ Permission is hereby granted, free of charge, to any person obtaining a copy
56
+ of this software and associated documentation files (the "Software"), to deal
57
+ in the Software without restriction, including without limitation the rights
58
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
59
+ copies of the Software, and to permit persons to whom the Software is
60
+ furnished to do so, subject to the following conditions:
61
+
62
+ The above copyright notice and this permission notice shall be included in all
63
+ copies or substantial portions of the Software.
64
+
65
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
66
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
67
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
68
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
69
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
70
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
71
+ SOFTWARE.
72
+
73
+ -----
package/README.md CHANGED
@@ -1,3 +1,106 @@
1
- # Temporary Holding Version
1
+ # Renor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ![Claude Code saves a decision to Renor; a fresh Codex session recalls it](docs/assets/renor-readme-demo.gif)\
4
+ *Self-hosted Renor, real session: Claude Code saves a decision, a fresh Codex session recalls it. Waiting time sped up.*
5
+
6
+ [![engine-public-ci](https://github.com/patrickpassosb/renor-engine/actions/workflows/engine-public-ci.yml/badge.svg?branch=main)](https://github.com/patrickpassosb/renor-engine/actions/workflows/engine-public-ci.yml)
7
+
8
+ **Renor is memory for coding agents: it keeps what you tell Claude Code, Codex or Cursor in a folder of Markdown files on your machine, and serves it back to any of them over MCP.**
9
+
10
+ Every new agent session starts from zero: you re-explain the project, last week's decision and why, the thing you corrected twice. With Renor, one agent saves it and the next one finds it. The memory is plain files you own. It runs locally, needs no account or API key, and sends nothing off your machine unless you configure a model. A hosted version with no install is at <https://renor.cloud>.
11
+
12
+ ## Quickstart
13
+
14
+ You need **Node 22** and git. The install is the only step that needs the network.
15
+
16
+ ```bash
17
+ git clone https://github.com/patrickpassosb/renor-engine.git
18
+ cd renor-engine
19
+ node -v # must print v22.x
20
+ npm ci && npm rebuild better-sqlite3 --ignore-scripts=false && npm run build
21
+ node scripts/smoke-self-hosted.mjs
22
+ ```
23
+
24
+ The last command starts the MCP server, saves one fact (`Ada favorite_color cerulean`), searches for it three ways and stops. It works in a temporary folder that it deletes afterwards, so it never touches your notes. It ends with:
25
+
26
+ ```text
27
+ PASS: self-hosted stdio MCP can remember and retrieve a current fact.
28
+ ```
29
+
30
+ Downloaded a ZIP instead of cloning? It has no git history to stamp the build with, so build with `RENOR_BUILD_COMMIT=0000000 npm run build` instead. A wrong Node version or skipped install scripts: see [Troubleshooting](docs/self-hosting.md#troubleshooting).
31
+
32
+ ## Connect your agent
33
+
34
+ **Without a clone.** With Node 22 installed, one line registers the published package. Your notes go to a hidden `.renor` folder in your home directory (`~/.renor`, printed on stderr at start; it never reuses an existing `renor` folder); to use another folder, add `-e VAULT_PATH=/absolute/path` to the `claude` line or `--env VAULT_PATH=/absolute/path` to the `codex` line. The first start downloads about 130 MB and takes about 15 seconds; if your agent shows one failed connection, restart it.
35
+
36
+ ```bash
37
+ claude mcp add --scope user renor-local -- npx -y renor-engine mcp
38
+ codex mcp add renor-local -- npx -y renor-engine mcp
39
+ ```
40
+
41
+ For Cursor or any other client, the server is the command `npx` with the arguments `-y renor-engine mcp`. `claude mcp remove renor-local` and `codex mcp remove renor-local` undo it. The rest of this section is the same thing from a clone, which also lets you change the code.
42
+
43
+ **From a clone.**
44
+
45
+ Pick a folder for your memory (a new, empty one is fine; an Obsidian vault works too) and index it. Run this, and the commands below, from the `renor-engine` directory in the same shell:
46
+
47
+ ```bash
48
+ export VAULT_PATH="$HOME/renor-notes" # absolute path; created if missing
49
+ node dist/cli.js index
50
+ ```
51
+
52
+ Each command below starts Renor with the Node 22 you are using now (`node -p process.execPath`), so the agent does not need Node 22 on its own `PATH`. In a new terminal, run the `export` line again first: each command stops with `VAULT_PATH: run the export line above first` rather than register a server that cannot start (an agent shows that only as "Connection closed").
53
+
54
+ **Claude Code**
55
+
56
+ ```bash
57
+ claude mcp add --scope user renor-local -e VAULT_PATH="${VAULT_PATH:?run the export line above first}" -- "$(node -p process.execPath)" "$PWD/dist/cli.js" mcp
58
+ ```
59
+
60
+ Restart Claude Code; `/mcp` lists `renor-local` in every project. `claude mcp remove renor-local` undoes it.
61
+
62
+ **Codex**
63
+
64
+ ```bash
65
+ codex mcp add renor-local --env VAULT_PATH="${VAULT_PATH:?run the export line above first}" -- "$(node -p process.execPath)" "$PWD/dist/cli.js" mcp
66
+ ```
67
+
68
+ Restart Codex; `codex mcp list` shows it. `codex mcp remove renor-local` undoes it.
69
+
70
+ **Cursor**
71
+
72
+ Cursor reads MCP servers from `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (one project). This prints the entry:
73
+
74
+ ```bash
75
+ VAULT_PATH="${VAULT_PATH:?run the export line above first}" node -e 'console.log(JSON.stringify({ mcpServers: { "renor-local": { command: process.execPath,
76
+ args: [process.cwd() + "/dist/cli.js", "mcp"], env: { VAULT_PATH: process.env.VAULT_PATH } } } }, null, 2))'
77
+ ```
78
+
79
+ Put it in that file, or merge the `renor-local` entry into an existing `mcpServers`, then check that `renor-local` is enabled in Cursor's MCP settings. (The `cursor-plugin/` folder of this repository is the Cursor plugin for the hosted service at renor.cloud, which needs a Renor account; it does not connect to this engine.)
80
+
81
+ **Any other MCP client:** the command is `node dist/cli.js mcp` (stdio) with `VAULT_PATH` set.
82
+
83
+ Then ask your agent to remember something, and ask about it in a new session. What an agent saves is searchable at once. A Markdown file you add or edit by hand is searchable after you run `node dist/cli.js index` again.
84
+
85
+ ## What is stored where
86
+
87
+ | What | Where |
88
+ | --- | --- |
89
+ | Your notes | Any Markdown files in `$VAULT_PATH`. Renor reads them; edit them with anything. |
90
+ | What agents save | Appended as Markdown in the same folder: facts to `System/Memory Events Ledger.md`, notes to `Memory/Daily/<date>.md`. |
91
+ | Search index | SQLite at `$VAULT_PATH/.semantic-memory/index.db`. Derived: `node dist/cli.js index` rebuilds it from the Markdown. |
92
+
93
+ Everything Renor keeps is in that one folder, so back it up, sync it or move it like any other folder. Nothing leaves your machine unless you set a model key: `EMBEDDING_PROVIDER=openai` sends your notes' text to OpenAI to embed it, and an evidence-model key (`RENOR_EVIDENCE_API_KEY` or an `OLLAMA_*` key) makes search send the notes it matched to that model. If your shell already exports such a key, add `-e RENOR_EVIDENCE_MODE=off` (Codex: `--env`) when you register the server. Every setting and its default: [docs/ENVIRONMENT-VARIABLES.md](docs/ENVIRONMENT-VARIABLES.md).
94
+
95
+ **Search without embeddings.** With nothing set, Renor uses no embedding model, so a note is found by its exact words: `blue notebook` finds "The blue notebook is on the desk", but `where is my notebook?` does not. Saved facts are found either way. For search by meaning, set `EMBEDDING_PROVIDER=openai` with `OPENAI_API_KEY`, or `local` to download a model once and run it on this machine, then run `node dist/cli.js rebuild` so existing notes are embedded ([details](docs/self-hosting.md#embeddings)).
96
+
97
+ ## More
98
+
99
+ - [docs/self-hosting.md](docs/self-hosting.md): troubleshooting, embeddings, the HTTP server with OAuth sign-in, connecting agents over HTTP, the tool lists, owner names, multi-agent ("hive") tools (set `RENOR_CONNECTION_NAME` per agent), and how the vault is protected.
100
+ - [skill/SKILL.md](skill/SKILL.md): instructions to give your agent so it recalls and saves memory without being asked. Most of it names the hosted tools (`get_context`, `remember`); the `renor-local` server above has other names (`search`, `remember_fact`, `note`), listed in its [Stdio section](skill/SKILL.md#stdio-renor-engine-mcp).
101
+
102
+ Renor is a single-owner engine: one person's notes, one set of agents. It is not a multi-user product.
103
+
104
+ ## Contributing, license, security
105
+
106
+ Contributions are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers setup and tests and lists good first issues. Renor's own code is **AGPL-3.0-only** ([LICENSE](LICENSE)); files imported from the upstream project stay ISC ([NOTICE](NOTICE), [UPSTREAM.md](UPSTREAM.md)). Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md), not in a public issue.
@@ -0,0 +1,6 @@
1
+ {
2
+ "commit": "19d71122d94bb2cd116cf594d23de16e7bdb2d7a",
3
+ "branch": "unknown",
4
+ "dirty": null,
5
+ "builtAt": "2026-10-20T20:23:44.240Z"
6
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LOCAL_API_HOSTNAMES = exports.API_TOKEN_ENV = void 0;
7
+ exports.requireApiToken = requireApiToken;
8
+ exports.bearerTokenGuard = bearerTokenGuard;
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ /** Environment variable holding the bearer token every `renor serve` route requires. */
11
+ exports.API_TOKEN_ENV = 'RENOR_API_TOKEN';
12
+ /** Host names the local API answers to. Anything else is a page rebinding its own domain to loopback. */
13
+ exports.LOCAL_API_HOSTNAMES = ['localhost', '127.0.0.1', '[::1]'];
14
+ function digest(value) {
15
+ return crypto_1.default.createHash('sha256').update(value, 'utf8').digest();
16
+ }
17
+ /**
18
+ * The token `renor serve` was started with, or a refusal naming the variable. A local API with no
19
+ * credential is open to every user of the machine (loopback is shared), so there is no default and no
20
+ * "open" mode: an unset or blank token is a start failure.
21
+ */
22
+ function requireApiToken(env = process.env) {
23
+ const token = env[exports.API_TOKEN_ENV]?.trim();
24
+ if (!token) {
25
+ throw new Error(`${exports.API_TOKEN_ENV} is not set. The HTTP API (renor serve) has no other protection: any user on this machine `
26
+ + 'can reach 127.0.0.1. Refusing to start without a bearer token (fail closed). '
27
+ + `Set ${exports.API_TOKEN_ENV} to a long random value (for example: export ${exports.API_TOKEN_ENV}=$(openssl rand -hex 32)) `
28
+ + 'and send it as "Authorization: Bearer <token>" on every request.');
29
+ }
30
+ return token;
31
+ }
32
+ /** Express middleware: `Authorization: Bearer <token>` is required, compared in constant time. */
33
+ function bearerTokenGuard(token) {
34
+ const expected = digest(token);
35
+ return (req, res, next) => {
36
+ const header = req.headers.authorization;
37
+ const match = typeof header === 'string' ? /^Bearer +(.+)$/i.exec(header) : null;
38
+ if (match && crypto_1.default.timingSafeEqual(digest(match[1].trim()), expected))
39
+ return next();
40
+ res.set('WWW-Authenticate', 'Bearer');
41
+ res.status(401).json({ error: 'unauthorized' });
42
+ };
43
+ }
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createServer = createServer;
7
+ const express_1 = __importDefault(require("express"));
8
+ const hostHeaderValidation_js_1 = require("@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js");
9
+ const apiAuth_1 = require("./apiAuth");
10
+ const orchestrator_1 = require("../retrieval/orchestrator");
11
+ const ledgerPresentation_1 = require("../retrieval/ledgerPresentation");
12
+ const entityLookup_1 = require("../retrieval/entityLookup");
13
+ const writer_1 = require("../memory/writer");
14
+ const lifecycle_1 = require("../memory/lifecycle");
15
+ const attach_1 = require("../evidence/attach");
16
+ const options_1 = require("../evidence/options");
17
+ const composer_1 = require("../context/composer");
18
+ const readHonesty_1 = require("../context/readHonesty");
19
+ const buildIdentity_1 = require("../buildIdentity");
20
+ function createServer(db, vectorIndex, provider, vaultPath, priorityPaths, memoryDir, ledgerPath, options) {
21
+ // Fail closed: there is no unauthenticated way to build this server.
22
+ const apiToken = options?.apiToken?.trim();
23
+ if (!apiToken)
24
+ throw new Error('createServer needs options.apiToken: the HTTP API does not run without a bearer token');
25
+ const app = (0, express_1.default)();
26
+ // DNS-rebinding guard first: a page that rebinds its own domain to 127.0.0.1 sends its own Host, and is refused
27
+ // before any route (or the token check) runs. Then the credential every other local user lacks.
28
+ app.use((0, hostHeaderValidation_js_1.hostHeaderValidation)(apiAuth_1.LOCAL_API_HOSTNAMES));
29
+ app.use((0, apiAuth_1.bearerTokenGuard)(apiToken));
30
+ app.use(express_1.default.json());
31
+ // Resolve the optional evidence path once per server, not once per request.
32
+ // This avoids opening a new SQLite evidence-cache connection on every search.
33
+ const productEvidenceEnabled = (0, attach_1.searchEvidenceEnabled)();
34
+ const productEvidenceOptions = productEvidenceEnabled
35
+ ? (0, options_1.resolveEvidenceToolOptions)({ dbPath: db.name })
36
+ : null;
37
+ const productEvidenceExtractor = productEvidenceOptions
38
+ ? (0, options_1.createEvidenceExtractor)(productEvidenceOptions)
39
+ : null;
40
+ // C0 shared composer (ADR-0003 §5): opt-in via RENOR_STRUCTURED_CONTEXT,
41
+ // default OFF — when off, responses are byte-identical to the pre-C0 contract.
42
+ const structuredOn = (0, composer_1.structuredContextEnabled)();
43
+ // Relative to the vault root — remember() joins it onto vaultPath.
44
+ const config_ledgerPath = ledgerPath ?? lifecycle_1.DEFAULT_LEDGER_REL;
45
+ // The ledger is the engine's own record, not a memory: this route returns neither its header nor the markup of its records.
46
+ const isLedger = (0, ledgerPresentation_1.ledgerNoteMatcher)({ memoryDir: vaultPath, ledgerRelPath: config_ledgerPath });
47
+ // Build/deploy identity: which source commit is this process running? Written
48
+ // at build time by scripts/ci/stamp-build.mjs and read through the shared,
49
+ // non-throwing helper (src/buildIdentity.ts) that the MVP's GET /version also
50
+ // uses. Deploy drift must be observable; the manifest never throws.
51
+ app.get('/api/health', (req, res) => {
52
+ res.json({ ok: true, name: 'renor', version: '1.0.0', ...(0, buildIdentity_1.readBuildIdentity)() });
53
+ });
54
+ app.post('/retrieve-context', async (req, res) => {
55
+ const { query, topK: rawTopK = 20 } = req.body;
56
+ if (!query)
57
+ return void res.status(400).json({ error: 'query required' });
58
+ const topK = Math.min(100, Math.max(1, Math.floor(Number(rawTopK) || 20)));
59
+ // LANE 3 read-time honesty (RENOR_READ_HONESTY, default OFF): provenance +
60
+ // contradiction + abstention alongside the unchanged hits array.
61
+ const readHonesty = (hits) => (0, readHonesty_1.readHonestyEnabled)()
62
+ ? {
63
+ readHonesty: (0, readHonesty_1.readHonestyForItems)({
64
+ db, query, items: hits, ledgerRelPath: config_ledgerPath,
65
+ }),
66
+ }
67
+ : {};
68
+ try {
69
+ const retrieved = await (0, orchestrator_1.retrieveContext)(db, vectorIndex, provider, query, topK, priorityPaths, {}, { isLedgerNote: isLedger });
70
+ if (!productEvidenceEnabled || !productEvidenceOptions) {
71
+ // Preserve the pre-evidence response contract exactly when the feature is off.
72
+ const plain = (0, ledgerPresentation_1.presentLedgerEvents)(retrieved.hits, isLedger);
73
+ return void res.json({
74
+ query: retrieved.query,
75
+ hits: plain,
76
+ ...(structuredOn
77
+ ? { composedContext: (0, composer_1.composeQueryContextFromDb)(db, { query: retrieved.query, hits: plain }) }
78
+ : {}),
79
+ ...readHonesty(plain),
80
+ });
81
+ }
82
+ const attached = await (0, attach_1.attachSearchEvidence)({
83
+ query,
84
+ hits: retrieved.hits,
85
+ vaultPath,
86
+ options: productEvidenceOptions,
87
+ extractor: productEvidenceExtractor,
88
+ enabled: true,
89
+ });
90
+ // The evidence pass read the sources as stored; what the caller is shown is what the user said.
91
+ const shown = (0, ledgerPresentation_1.presentLedgerEvents)(attached.hits, isLedger);
92
+ res.json({
93
+ query: retrieved.query,
94
+ hits: shown,
95
+ evidence: attached.evidence,
96
+ ...(structuredOn
97
+ ? { composedContext: (0, composer_1.composeQueryContextFromDb)(db, { query: retrieved.query, hits: shown.filter((h) => h.notePath !== attach_1.EVIDENCE_HIT_PATH), evidence: attached.evidence }) }
98
+ : {}),
99
+ ...readHonesty(shown),
100
+ });
101
+ }
102
+ catch (e) {
103
+ res.status(500).json({ error: e.message });
104
+ }
105
+ });
106
+ app.get('/entity/:name', (req, res) => {
107
+ const entity = (0, entityLookup_1.lookupEntity)(db, req.params.name);
108
+ if (!entity)
109
+ return void res.status(404).json({ error: 'not found' });
110
+ res.json(entity);
111
+ });
112
+ app.get('/facts/:entityId', (req, res) => {
113
+ // Current-only by default (temporal-memory contract): superseded facts carry
114
+ // valid_to and are opt-in via history/asOf endpoints, never the ID route.
115
+ const rows = db.prepare('SELECT * FROM facts WHERE subject_entity_id = ? AND valid_to IS NULL ORDER BY updated_at DESC').all(Number(req.params.entityId));
116
+ res.json(rows);
117
+ });
118
+ app.post('/memory/daily', async (req, res) => {
119
+ const { date, text, source = 'api' } = req.body;
120
+ if (!date || !text)
121
+ return void res.status(400).json({ error: 'date and text required' });
122
+ try {
123
+ await (0, writer_1.appendDailyMemory)(vaultPath, date, { text, source }, memoryDir);
124
+ res.json({ ok: true });
125
+ }
126
+ catch (e) {
127
+ res.status(500).json({ error: e.message });
128
+ }
129
+ });
130
+ // Lifecycle-backed durable memory (ledger-first, supersedes, idempotent, returns receipt).
131
+ app.post('/memory/fact', async (req, res) => {
132
+ const { subject, predicate, object, source } = req.body;
133
+ if (!subject || !predicate || !object)
134
+ return void res.status(400).json({ error: 'subject, predicate, object required' });
135
+ try {
136
+ res.json(await (0, lifecycle_1.remember)(db, vaultPath, config_ledgerPath, { subject, predicate, object, source }));
137
+ }
138
+ catch (e) {
139
+ // A rejected lifecycle FIELD is the caller's mistake, not an engine fault: remember()
140
+ // validates every field before touching the ledger, so this is a bad request and must
141
+ // answer 4xx. Answering 500 made a client-side type bug (a non-string `object`) look like
142
+ // a server outage. Engine faults — unreadable ledger, reused event id, clock regression,
143
+ // an escaping ledger path — still throw plain Errors and stay 500.
144
+ if (e instanceof lifecycle_1.LifecycleInputError)
145
+ return void res.status(400).json({ error: e.message });
146
+ res.status(500).json({ error: e.message });
147
+ }
148
+ });
149
+ // Facts by entity NAME (agents don't know DB ids). ?asOf=ISO&history=1
150
+ app.get('/facts-for/:name', (req, res) => {
151
+ try {
152
+ res.json((0, lifecycle_1.factsFor)(db, req.params.name, {
153
+ asOf: req.query.asOf,
154
+ includeHistory: req.query.history === '1' || req.query.history === 'true',
155
+ }));
156
+ }
157
+ catch (e) {
158
+ res.status(500).json({ error: e.message });
159
+ }
160
+ });
161
+ // One generic answer for anything a route did not handle: Express's own error page names the absolute path of the
162
+ // file that threw. The cause stays in this process's log.
163
+ app.use((err, _req, res, next) => {
164
+ if (res.headersSent)
165
+ return next(err);
166
+ const status = Number.isInteger(err?.status) && err.status >= 400 && err.status < 500 ? err.status : 500;
167
+ if (status >= 500)
168
+ console.error('[serve] request failed:', err);
169
+ res.status(status).json({ error: status >= 500 ? 'internal error' : 'bad request' });
170
+ });
171
+ return app;
172
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUILD_MANIFEST_PATH = exports.MAX_BUILD_MANIFEST_BYTES = void 0;
4
+ exports.readBuildIdentity = readBuildIdentity;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const UNKNOWN_BUILD_IDENTITY = Object.freeze({
8
+ commit: 'unknown',
9
+ branch: 'unknown',
10
+ dirty: null,
11
+ builtAt: null,
12
+ });
13
+ /**
14
+ * Manifest size gate. A stamp written by scripts/ci/stamp-build.mjs is ~200
15
+ * bytes, so anything in the KiB range is not a build stamp this project wrote.
16
+ * Refusing it by size (before the contents are even parsed) bounds the work and
17
+ * the memory the reader can be forced into, and keeps a giant file from ever
18
+ * becoming the input to a response.
19
+ */
20
+ exports.MAX_BUILD_MANIFEST_BYTES = 4096;
21
+ /** A git revision id as printed by `git rev-parse HEAD`, shortened or full. */
22
+ const COMMIT_PATTERN = /^[0-9a-fA-F]{7,40}$/;
23
+ /**
24
+ * ISO-8601 instant: calendar date, time to the second, optional fractional
25
+ * seconds, and either `Z` or a numeric offset — the shapes `Date#toISOString`
26
+ * and ordinary ISO producers emit. Anything else (partial dates, prose,
27
+ * dates with embedded whitespace) is not a build timestamp.
28
+ */
29
+ const BUILT_AT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/;
30
+ /** Longest ISO instant worth accepting (offset + 6 fractional digits). */
31
+ const MAX_BUILT_AT_LENGTH = 35;
32
+ /** Git ref names are ASCII: letters, digits, `.`, `_`, `-` and `/` separators. */
33
+ const BRANCH_PATTERN = /^[A-Za-z0-9._/-]+$/;
34
+ /** git's own per-ref-component cap is 255 bytes; nothing legitimate is longer. */
35
+ const MAX_BRANCH_LENGTH = 255;
36
+ /** The engine package root: nearest ancestor of this module holding package.json. */
37
+ function resolveEngineRoot() {
38
+ let dir = __dirname;
39
+ for (let up = 0; up < 3; up += 1) {
40
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'package.json')))
41
+ return dir;
42
+ const parent = (0, node_path_1.dirname)(dir);
43
+ if (parent === dir)
44
+ break;
45
+ dir = parent;
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Absolute path of the stamped manifest, or null when the package root cannot
51
+ * be located (then identity is simply unknown — never a guess).
52
+ *
53
+ * Resolved from the package root rather than a fixed relative hop because the
54
+ * two layouts differ: a compiled deployment loads this module from dist/ while
55
+ * a source run (tsx: `npm run mvp`, `renor serve`) loads it from src/, and both
56
+ * must find <engine>/dist/BUILD.json.
57
+ */
58
+ exports.BUILD_MANIFEST_PATH = (() => {
59
+ const root = resolveEngineRoot();
60
+ return root === null ? null : (0, node_path_1.join)(root, 'dist', 'BUILD.json');
61
+ })();
62
+ /** Hex revision id, or the unknown placeholder. */
63
+ function validatedCommit(value) {
64
+ return typeof value === 'string' && COMMIT_PATTERN.test(value)
65
+ ? value
66
+ : UNKNOWN_BUILD_IDENTITY.commit;
67
+ }
68
+ /**
69
+ * Plausible git ref name, or the unknown placeholder. Enforces the
70
+ * `git check-ref-format` rules a charset check alone would miss (`..`, `@{`,
71
+ * leading dot, `.lock` suffix, empty component), so a filesystem path, a tenant
72
+ * slug, a URL or a ref-shaped injection cannot pass as a branch.
73
+ */
74
+ function validatedBranch(value) {
75
+ const unknown = UNKNOWN_BUILD_IDENTITY.branch;
76
+ if (typeof value !== 'string' || value.length > MAX_BRANCH_LENGTH)
77
+ return unknown;
78
+ if (!BRANCH_PATTERN.test(value))
79
+ return unknown;
80
+ if (value.includes('..') || value.includes('@{'))
81
+ return unknown;
82
+ for (const component of value.split('/')) {
83
+ if (component === '' || component.startsWith('.'))
84
+ return unknown;
85
+ if (component.endsWith('.lock'))
86
+ return unknown;
87
+ }
88
+ return value;
89
+ }
90
+ /** Parsable ISO-8601 instant, or null. */
91
+ function validatedBuiltAt(value) {
92
+ if (typeof value !== 'string' || value.length > MAX_BUILT_AT_LENGTH)
93
+ return null;
94
+ if (!BUILT_AT_PATTERN.test(value))
95
+ return null;
96
+ return Number.isFinite(Date.parse(value)) ? value : null;
97
+ }
98
+ /**
99
+ * Reads the build manifest. Returns the unknown identity — never throws — when
100
+ * the manifest is absent, unreadable, oversized, not JSON, not an object, or
101
+ * carries a value that fails its field's shape check.
102
+ */
103
+ function readBuildIdentity(manifestPath = exports.BUILD_MANIFEST_PATH) {
104
+ if (manifestPath === null)
105
+ return { ...UNKNOWN_BUILD_IDENTITY };
106
+ let raw;
107
+ try {
108
+ if ((0, node_fs_1.statSync)(manifestPath).size > exports.MAX_BUILD_MANIFEST_BYTES) {
109
+ return { ...UNKNOWN_BUILD_IDENTITY }; // not a stamp we wrote
110
+ }
111
+ raw = (0, node_fs_1.readFileSync)(manifestPath, 'utf8');
112
+ }
113
+ catch {
114
+ return { ...UNKNOWN_BUILD_IDENTITY }; // pre-stamp / snapshot deployment
115
+ }
116
+ try {
117
+ const parsed = JSON.parse(raw);
118
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
119
+ return { ...UNKNOWN_BUILD_IDENTITY };
120
+ }
121
+ const manifest = parsed;
122
+ return {
123
+ commit: validatedCommit(manifest.commit),
124
+ branch: validatedBranch(manifest.branch),
125
+ dirty: typeof manifest.dirty === 'boolean' ? manifest.dirty : null,
126
+ builtAt: validatedBuiltAt(manifest.builtAt),
127
+ };
128
+ }
129
+ catch {
130
+ return { ...UNKNOWN_BUILD_IDENTITY };
131
+ }
132
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildWritePathExtractors = buildWritePathExtractors;
7
+ const openai_1 = __importDefault(require("openai"));
8
+ const factExtractor_1 = require("./extraction/factExtractor");
9
+ const extractionClient_1 = require("./extraction/extractionClient");
10
+ const typedFacts_1 = require("./extraction/typedFacts");
11
+ const eventExtractor_1 = require("./extraction/eventExtractor");
12
+ const preferenceExtractor_1 = require("./extraction/preferenceExtractor");
13
+ /**
14
+ * Structured-row lanes use only a named extractor provider. A bare
15
+ * OPENAI_API_KEY does not arm them. LLM_EXTRACTION is the older fact path:
16
+ * when that flag is on and no named provider exists, it keeps the OpenAI
17
+ * client main already built from that key.
18
+ *
19
+ * The legacy fact path asks for `FACTS_MODEL` only when the resolved provider is
20
+ * one of the extractor-specific providers. On the generic OPENAI_API_KEY +
21
+ * OPENAI_BASE_URL fallback it keeps the fact extractor's own `gpt-4o-mini`
22
+ * default, which is the model `main` sent there. See
23
+ * `isExtractorSpecificProvider`.
24
+ */
25
+ function buildWritePathExtractors(config, env = process.env) {
26
+ const resolved = (0, extractionClient_1.openExtractorClient)(typedFacts_1.FACTS_MODEL, env);
27
+ const extractorClient = resolved?.client ?? null;
28
+ const legacyFactClient = config.llmExtraction && !extractorClient && config.openaiApiKey
29
+ ? new openai_1.default({ apiKey: config.openaiApiKey })
30
+ : null;
31
+ const factClient = (extractorClient ?? legacyFactClient);
32
+ return {
33
+ factExtractor: config.llmExtraction && factClient
34
+ ? (title, body) => (0, factExtractor_1.extractFacts)(factClient, title, body, (0, extractionClient_1.isExtractorSpecificProvider)(resolved?.config ?? null) ? typedFacts_1.FACTS_MODEL : 'gpt-4o-mini')
35
+ : undefined,
36
+ numericFactExtractor: (0, typedFacts_1.typedFactsEnabled)(env) && extractorClient
37
+ ? (title, body) => (0, typedFacts_1.extractNumericFacts)(extractorClient, title, body)
38
+ : undefined,
39
+ eventExtractor: (0, eventExtractor_1.chronicleEnabled)(env) && extractorClient
40
+ ? (title, body) => (0, eventExtractor_1.extractEvents)(extractorClient, title, body)
41
+ : undefined,
42
+ preferenceExtractor: (0, preferenceExtractor_1.prefsEnabled)(env) && extractorClient
43
+ ? (title, body) => (0, preferenceExtractor_1.extractPreferences)(extractorClient, title, body)
44
+ : undefined,
45
+ };
46
+ }