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.
- package/LICENSE +661 -0
- package/NOTICE +73 -0
- package/README.md +105 -2
- package/dist/BUILD.json +6 -0
- package/dist/api/apiAuth.js +43 -0
- package/dist/api/server.js +172 -0
- package/dist/buildIdentity.js +132 -0
- package/dist/cli-extractors.js +46 -0
- package/dist/cli.js +491 -0
- package/dist/config.js +65 -0
- package/dist/context/composer.js +320 -0
- package/dist/context/readHonesty.js +478 -0
- package/dist/context/rowStore.js +238 -0
- package/dist/conversation/formatter.js +81 -0
- package/dist/conversation/parser.js +189 -0
- package/dist/conversation/types.js +2 -0
- package/dist/conversation/writer.js +42 -0
- package/dist/db/client.js +147 -0
- package/dist/db/indexSalvage.js +81 -0
- package/dist/db/migrations.js +642 -0
- package/dist/db/readCache.js +109 -0
- package/dist/db/schema.js +5 -0
- package/dist/db/statements.js +89 -0
- package/dist/digests/digestCache.js +142 -0
- package/dist/digests/digests.js +597 -0
- package/dist/digests/worker.js +348 -0
- package/dist/embeddings/caching.js +337 -0
- package/dist/embeddings/coalescing.js +177 -0
- package/dist/embeddings/interactive.js +103 -0
- package/dist/embeddings/local.js +68 -0
- package/dist/embeddings/openai.js +105 -0
- package/dist/embeddings/rebuild.js +166 -0
- package/dist/embeddings/simdScan.js +258 -0
- package/dist/embeddings/space.js +21 -0
- package/dist/embeddings/types.js +14 -0
- package/dist/embeddings/vectorIndex.js +675 -0
- package/dist/embeddings/vertex.js +224 -0
- package/dist/eval/artifacts.js +42 -0
- package/dist/eval/cacheReport.js +139 -0
- package/dist/eval/comparison.js +182 -0
- package/dist/eval/context700-candidate.js +145 -0
- package/dist/eval/evalCache.js +525 -0
- package/dist/eval/hashEmbedding.js +35 -0
- package/dist/eval/longmemeval.js +56 -0
- package/dist/eval/metrics.js +181 -0
- package/dist/eval/renorbenchBaseline.js +486 -0
- package/dist/eval/renorbenchDocuments.js +134 -0
- package/dist/eval/renorbenchProduct.js +182 -0
- package/dist/eval/runLedger.js +279 -0
- package/dist/eval/runManifest.js +479 -0
- package/dist/eval/scoring.js +197 -0
- package/dist/evidence/attach.js +267 -0
- package/dist/evidence/background.js +147 -0
- package/dist/evidence/evidenceCache.js +182 -0
- package/dist/evidence/gate.js +37 -0
- package/dist/evidence/keyPool.js +85 -0
- package/dist/evidence/ollamaChat.js +84 -0
- package/dist/evidence/optin.js +165 -0
- package/dist/evidence/options.js +130 -0
- package/dist/evidence/secondPass.js +456 -0
- package/dist/extraction/classifyCapture.js +34 -0
- package/dist/extraction/decision-model.js +174 -0
- package/dist/extraction/eventExtractor.js +113 -0
- package/dist/extraction/extractionClient.js +122 -0
- package/dist/extraction/factExtractor.js +46 -0
- package/dist/extraction/factsWritePath.js +179 -0
- package/dist/extraction/preferenceExtractor.js +153 -0
- package/dist/extraction/secretGuard.js +176 -0
- package/dist/extraction/secretScanner.js +665 -0
- package/dist/extraction/typedFacts.js +224 -0
- package/dist/extraction/writePathAdjudication.js +110 -0
- package/dist/importing/apply.js +644 -0
- package/dist/importing/bundle-write.js +80 -0
- package/dist/importing/cli.js +407 -0
- package/dist/importing/existing.js +49 -0
- package/dist/importing/facts.js +123 -0
- package/dist/importing/forgetImported.js +79 -0
- package/dist/importing/hosted.js +467 -0
- package/dist/importing/index.js +29 -0
- package/dist/importing/journalLog.js +96 -0
- package/dist/importing/markdown.js +98 -0
- package/dist/importing/plan.js +206 -0
- package/dist/importing/secretScreen.js +113 -0
- package/dist/importing/serialize.js +188 -0
- package/dist/importing/source.js +463 -0
- package/dist/importing/types.js +6 -0
- package/dist/indexer/chunker.js +319 -0
- package/dist/indexer/dailyEntries.js +63 -0
- package/dist/indexer/hasher.js +10 -0
- package/dist/indexer/laneRuns.js +48 -0
- package/dist/indexer/o200k.js +481 -0
- package/dist/indexer/o200k.json +8 -0
- package/dist/indexer/outsideRoot.js +77 -0
- package/dist/indexer/parser.js +167 -0
- package/dist/indexer/pipeline.js +1168 -0
- package/dist/indexer/staleness.js +201 -0
- package/dist/indexer/tokenizer.js +77 -0
- package/dist/indexer/walker.js +184 -0
- package/dist/mcp/hive.js +88 -0
- package/dist/mcp/requestErrors.js +47 -0
- package/dist/mcp/server.js +396 -0
- package/dist/memory/agentLease.js +43 -0
- package/dist/memory/confinedWrite.js +315 -0
- package/dist/memory/controls.js +73 -0
- package/dist/memory/dailyNote.js +265 -0
- package/dist/memory/durableAppend.js +57 -0
- package/dist/memory/factConflicts.js +203 -0
- package/dist/memory/factTerms.js +321 -0
- package/dist/memory/hiveNameHeader.cjs +10 -0
- package/dist/memory/inventory.js +55 -0
- package/dist/memory/invisibleText.js +20 -0
- package/dist/memory/lifecycle.js +1756 -0
- package/dist/memory/limits.js +13 -0
- package/dist/memory/requests.js +183 -0
- package/dist/memory/sourceName.js +200 -0
- package/dist/memory/whatsNew.js +158 -0
- package/dist/memory/writer.js +175 -0
- package/dist/memory/writerConflicts.js +85 -0
- package/dist/memorymap/memoryMap.js +1568 -0
- package/dist/memorymap/replacedJournal.js +58 -0
- package/dist/memorymap/replacements.js +405 -0
- package/dist/mvp/activity.js +86 -0
- package/dist/mvp/agentMemory.js +1744 -0
- package/dist/mvp/auth.js +531 -0
- package/dist/mvp/cli.js +73 -0
- package/dist/mvp/client-store.js +462 -0
- package/dist/mvp/compileCache.js +249 -0
- package/dist/mvp/compileSeed.js +27 -0
- package/dist/mvp/config.js +288 -0
- package/dist/mvp/connectionStatus.js +80 -0
- package/dist/mvp/contextRelevance.js +413 -0
- package/dist/mvp/engine.js +1065 -0
- package/dist/mvp/exportAll.js +359 -0
- package/dist/mvp/factLanePack.js +77 -0
- package/dist/mvp/factMarkers.js +49 -0
- package/dist/mvp/factsLane.js +203 -0
- package/dist/mvp/hiddenSpans.js +585 -0
- package/dist/mvp/history.js +413 -0
- package/dist/mvp/hitDates.js +31 -0
- package/dist/mvp/hitFloor.js +36 -0
- package/dist/mvp/identity.js +18 -0
- package/dist/mvp/idle.js +113 -0
- package/dist/mvp/memoryDir.js +98 -0
- package/dist/mvp/posthogMcp.js +210 -0
- package/dist/mvp/readScope.js +104 -0
- package/dist/mvp/readUsefulness.js +222 -0
- package/dist/mvp/sanitize.js +48 -0
- package/dist/mvp/schemaRejections.js +91 -0
- package/dist/mvp/scope.js +127 -0
- package/dist/mvp/server.js +2089 -0
- package/dist/mvp/serverCard.js +29 -0
- package/dist/mvp/storedSize.js +139 -0
- package/dist/mvp/telemetry.js +441 -0
- package/dist/retrieval/adviceLines.js +195 -0
- package/dist/retrieval/adviceQuestion.js +44 -0
- package/dist/retrieval/conversationShape.js +44 -0
- package/dist/retrieval/dailyEntryTime.js +55 -0
- package/dist/retrieval/entityLookup.js +113 -0
- package/dist/retrieval/factsFirst.js +188 -0
- package/dist/retrieval/keyword.js +84 -0
- package/dist/retrieval/ledgerEventTime.js +51 -0
- package/dist/retrieval/ledgerLiveness.js +83 -0
- package/dist/retrieval/ledgerPresentation.js +161 -0
- package/dist/retrieval/noteIdentity.js +57 -0
- package/dist/retrieval/orchestrator.js +1086 -0
- package/dist/retrieval/queryHelpers.js +142 -0
- package/dist/retrieval/questionSpan.js +475 -0
- package/dist/retrieval/readerEvents.js +690 -0
- package/dist/retrieval/readerLayout.js +503 -0
- package/dist/retrieval/relativeTimeHints.js +113 -0
- package/dist/retrieval/semantic.js +67 -0
- package/dist/router/routerCache.js +106 -0
- package/dist/router/sessionRouter.js +726 -0
- package/dist/toolRefusal.js +120 -0
- package/dist/watcher/watcher.js +75 -0
- 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
|
-
#
|
|
1
|
+
# Renor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
\
|
|
4
|
+
*Self-hosted Renor, real session: Claude Code saves a decision, a fresh Codex session recalls it. Waiting time sped up.*
|
|
5
|
+
|
|
6
|
+
[](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.
|
package/dist/BUILD.json
ADDED
|
@@ -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
|
+
}
|