recallnest 2.6.1 → 3.0.1
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/CHANGELOG.md +292 -0
- package/README.md +280 -195
- package/README_CN.md +266 -199
- package/config.json.example +8 -1
- package/docs/api-reference.md +1 -1
- package/integrations/README.md +1 -1
- package/integrations/agy/README.md +73 -0
- package/integrations/agy/setup.sh +75 -0
- package/integrations/setup-all.sh +1 -1
- package/package.json +7 -5
- package/src/access-tracker.ts +10 -1
- package/src/activity-counter.ts +69 -11
- package/src/age-label.ts +29 -0
- package/src/audit-log.ts +35 -0
- package/src/auto-gc.ts +97 -30
- package/src/capture-engine.ts +26 -4
- package/src/cli.ts +224 -10
- package/src/consolidation-engine.ts +317 -123
- package/src/context-composer-stable.ts +5 -1
- package/src/data-checkup.ts +17 -3
- package/src/dream-pipeline.ts +90 -12
- package/src/embedder.ts +44 -5
- package/src/env-config.ts +71 -0
- package/src/forget-engine.ts +8 -2
- package/src/gateway-server.ts +272 -0
- package/src/ingest.ts +248 -22
- package/src/kg-extractor.ts +17 -0
- package/src/llm-client.ts +86 -25
- package/src/mcp-server.ts +2 -1
- package/src/mcp-tools-advanced.ts +7 -2
- package/src/mcp-tools-core.ts +11 -1
- package/src/mcp-tools-governance.ts +45 -3
- package/src/memory-evolution.ts +16 -0
- package/src/memory-output.ts +47 -8
- package/src/memory-promotion.ts +280 -0
- package/src/memory-store-port.ts +32 -3
- package/src/memory-utility.ts +314 -0
- package/src/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/recall-ledger.ts +76 -0
- package/src/retriever.ts +82 -9
- package/src/runtime-config.ts +9 -0
- package/src/scope-policy.ts +22 -1
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +13 -4
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +218 -1
- package/src/store.ts +231 -71
- package/src/synthesis-contract.ts +286 -0
- package/src/version-manager.ts +83 -0
- package/src/workflow-observation-schema.ts +26 -0
- package/integrations/gemini-cli/README.md +0 -57
- package/integrations/gemini-cli/setup.sh +0 -64
- package/src/__tests__/access-tracker-novelty.test.ts +0 -304
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -945
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -430
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -276
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -780
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config.test.ts +0 -151
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -189
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -250
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -205
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -839
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
- /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* RecallNest Read-Only Gateway
|
|
4
|
+
*
|
|
5
|
+
* A thin, authenticated, read-only front door for the RecallNest HTTP API.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists as a separate process:
|
|
8
|
+
* `api-server.ts` deliberately binds to 127.0.0.1 AND rejects any request whose
|
|
9
|
+
* Host header is not local (`enforceLocalHttpRequestPolicy`). That is a load-bearing
|
|
10
|
+
* safety property — it also exposes write routes (`/v1/store`, `/v1/checkpoint`).
|
|
11
|
+
* Exposing it directly to a phone or a tunnel would hand out write access.
|
|
12
|
+
* This gateway sits in front instead: it authenticates, rate-limits, allows only
|
|
13
|
+
* read routes, and forwards to the local API over the loopback interface.
|
|
14
|
+
*
|
|
15
|
+
* Intended use: put it behind a tunnel (Tailscale Serve/Funnel, Cloudflare Tunnel, …)
|
|
16
|
+
* so an AI app on a phone can read the same memory your desktop agents use.
|
|
17
|
+
*
|
|
18
|
+
* Design constraints (each is deliberate — think before changing):
|
|
19
|
+
* - Binds to 127.0.0.1 by default. Reaching it from outside is the tunnel's job,
|
|
20
|
+
* not this process's. Overriding the host is possible but unwise.
|
|
21
|
+
* - Read-only. The forward allow-list has no write route in it, and requests to
|
|
22
|
+
* anything not on the list are rejected before any forwarding happens.
|
|
23
|
+
* - Bearer token required, compared in constant time, and refused if too short.
|
|
24
|
+
* - Rate limited, with a hard cap on response size.
|
|
25
|
+
* - Optional file search roots are an explicit allow-list; queries are passed to
|
|
26
|
+
* ripgrep as an argv element, never through a shell.
|
|
27
|
+
*
|
|
28
|
+
* Start: bun run gateway
|
|
29
|
+
* Env:
|
|
30
|
+
* RECALLNEST_GATEWAY_PORT default 8791
|
|
31
|
+
* RECALLNEST_GATEWAY_HOST default 127.0.0.1
|
|
32
|
+
* RECALLNEST_GATEWAY_TOKEN bearer token (>= 32 chars); or put it in
|
|
33
|
+
* ~/.config/recallnest/gateway-token
|
|
34
|
+
* RECALLNEST_API_URL default http://127.0.0.1:4318
|
|
35
|
+
* RECALLNEST_GATEWAY_RATE_MAX default 30 (requests per minute)
|
|
36
|
+
* RECALLNEST_GATEWAY_FILE_ROOTS optional, enables GET /files/search
|
|
37
|
+
* format: name=/abs/path,name=/abs/path
|
|
38
|
+
* RECALLNEST_GATEWAY_RG path to ripgrep (default: rg on PATH)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { timingSafeEqual, randomUUID } from "node:crypto";
|
|
42
|
+
import { readFileSync, appendFile, existsSync, statSync } from "node:fs";
|
|
43
|
+
import { homedir } from "node:os";
|
|
44
|
+
import { join, isAbsolute } from "node:path";
|
|
45
|
+
import { execFile } from "node:child_process";
|
|
46
|
+
import * as envConfig from "./env-config.js";
|
|
47
|
+
|
|
48
|
+
const HOME = homedir();
|
|
49
|
+
const PORT = clampInt(envConfig.gatewayPortRaw(), 8791, 1, 65535);
|
|
50
|
+
const HOST = envConfig.gatewayHostRaw()?.trim() || "127.0.0.1";
|
|
51
|
+
const API_URL = (envConfig.apiUrlRaw()?.trim() || "http://127.0.0.1:4318").replace(/\/+$/, "");
|
|
52
|
+
const RATE_MAX = clampInt(envConfig.gatewayRateMaxRaw(), 30, 1, 10_000);
|
|
53
|
+
const RATE_WINDOW_MS = 60_000;
|
|
54
|
+
const MAX_RESPONSE_BYTES = 256 * 1024;
|
|
55
|
+
const MAX_REQUEST_BYTES = 64 * 1024;
|
|
56
|
+
const RG = envConfig.gatewayRgRaw()?.trim() || "rg";
|
|
57
|
+
const LOG_FILE = join(HOME, ".config", "recallnest", "gateway-access.log");
|
|
58
|
+
|
|
59
|
+
function clampInt(raw: string | undefined, fallback: number, min: number, max: number): number {
|
|
60
|
+
const n = Number(raw);
|
|
61
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) return fallback;
|
|
62
|
+
return Math.min(Math.max(n, min), max);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── token ────────────────────────────────────────────────────────────────────
|
|
66
|
+
function loadToken(): string {
|
|
67
|
+
const fromEnv = envConfig.gatewayTokenRaw()?.trim();
|
|
68
|
+
if (fromEnv) return fromEnv;
|
|
69
|
+
const file = join(HOME, ".config", "recallnest", "gateway-token");
|
|
70
|
+
if (existsSync(file)) return readFileSync(file, "utf8").trim();
|
|
71
|
+
throw new Error(
|
|
72
|
+
"gateway: no token. Set RECALLNEST_GATEWAY_TOKEN or write ~/.config/recallnest/gateway-token",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const TOKEN = loadToken();
|
|
77
|
+
if (TOKEN.length < 32) {
|
|
78
|
+
throw new Error("gateway: token shorter than 32 chars — refusing to start");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── optional file search roots ───────────────────────────────────────────────
|
|
82
|
+
function parseFileRoots(): Record<string, string> {
|
|
83
|
+
const raw = envConfig.gatewayFileRootsRaw()?.trim();
|
|
84
|
+
if (!raw) return {};
|
|
85
|
+
const roots: Record<string, string> = {};
|
|
86
|
+
for (const pair of raw.split(",")) {
|
|
87
|
+
const [name, ...rest] = pair.split("=");
|
|
88
|
+
const key = name?.trim();
|
|
89
|
+
const dir = rest.join("=").trim();
|
|
90
|
+
if (!key || !dir) continue;
|
|
91
|
+
if (!isAbsolute(dir)) throw new Error(`gateway: file root "${key}" must be an absolute path`);
|
|
92
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
93
|
+
throw new Error(`gateway: file root "${key}" is not an existing directory: ${dir}`);
|
|
94
|
+
}
|
|
95
|
+
roots[key] = dir;
|
|
96
|
+
}
|
|
97
|
+
return roots;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const FILE_ROOTS = parseFileRoots();
|
|
101
|
+
const FILE_SEARCH_ENABLED = Object.keys(FILE_ROOTS).length > 0;
|
|
102
|
+
|
|
103
|
+
// ── read-only forward allow-list ─────────────────────────────────────────────
|
|
104
|
+
// Anything not listed here is a 404 before a single byte is forwarded.
|
|
105
|
+
const FORWARD_GET: Record<string, string> = {
|
|
106
|
+
"/stats": "/v1/stats",
|
|
107
|
+
"/health": "/v1/health",
|
|
108
|
+
};
|
|
109
|
+
const FORWARD_POST: Record<string, string> = {
|
|
110
|
+
"/recall": "/v1/recall",
|
|
111
|
+
"/search": "/v1/search",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ── auth / rate limiting ─────────────────────────────────────────────────────
|
|
115
|
+
const wantToken = Buffer.from(TOKEN);
|
|
116
|
+
|
|
117
|
+
function authOk(header: string | null): boolean {
|
|
118
|
+
if (!header || !header.startsWith("Bearer ")) return false;
|
|
119
|
+
const given = Buffer.from(header.slice(7).trim());
|
|
120
|
+
if (given.length !== wantToken.length) return false;
|
|
121
|
+
return timingSafeEqual(given, wantToken);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const rateHits: number[] = [];
|
|
125
|
+
function rateOk(): boolean {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
while (rateHits.length && now - rateHits[0]! >= RATE_WINDOW_MS) rateHits.shift();
|
|
128
|
+
if (rateHits.length >= RATE_MAX) return false;
|
|
129
|
+
rateHits.push(now);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function log(entry: Record<string, unknown>): void {
|
|
134
|
+
appendFile(LOG_FILE, JSON.stringify(entry) + "\n", () => {});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function json(status: number, body: unknown): Response {
|
|
138
|
+
let payload = JSON.stringify(body);
|
|
139
|
+
if (payload.length > MAX_RESPONSE_BYTES) {
|
|
140
|
+
payload = JSON.stringify({ error: "response too large", limit: MAX_RESPONSE_BYTES });
|
|
141
|
+
status = 502;
|
|
142
|
+
}
|
|
143
|
+
return new Response(payload, {
|
|
144
|
+
status,
|
|
145
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── forwarding ───────────────────────────────────────────────────────────────
|
|
150
|
+
async function forward(target: string, method: "GET" | "POST", body?: string): Promise<Response> {
|
|
151
|
+
const init: RequestInit = { method };
|
|
152
|
+
if (method === "POST") {
|
|
153
|
+
init.headers = { "content-type": "application/json" };
|
|
154
|
+
init.body = body ?? "{}";
|
|
155
|
+
}
|
|
156
|
+
const upstream = await fetch(`${API_URL}${target}`, init);
|
|
157
|
+
const text = await upstream.text();
|
|
158
|
+
if (text.length > MAX_RESPONSE_BYTES) {
|
|
159
|
+
return json(502, { error: "upstream response too large", limit: MAX_RESPONSE_BYTES });
|
|
160
|
+
}
|
|
161
|
+
return new Response(text, {
|
|
162
|
+
status: upstream.status,
|
|
163
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── optional ripgrep file search ─────────────────────────────────────────────
|
|
168
|
+
interface FileHit { file: string; line: number; text: string }
|
|
169
|
+
|
|
170
|
+
function fileSearch(query: string, scope: string, limit: number): Promise<FileHit[]> {
|
|
171
|
+
const targets = scope === "all" ? Object.values(FILE_ROOTS) : [FILE_ROOTS[scope]!];
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
// argv form: the query is always one element, never touched by a shell
|
|
174
|
+
const args = ["--json", "--max-count", "3", "--max-filesize", "2M", "-i", "--", query, ...targets];
|
|
175
|
+
execFile(RG, args, { maxBuffer: 8 * 1024 * 1024, timeout: 20_000 }, (err, stdout) => {
|
|
176
|
+
// ripgrep exits 1 when there is no match — not an error
|
|
177
|
+
if (err && (err as NodeJS.ErrnoException).code !== 1 && !stdout) return reject(err);
|
|
178
|
+
const hits: FileHit[] = [];
|
|
179
|
+
for (const line of String(stdout).split("\n")) {
|
|
180
|
+
if (!line.trim() || hits.length >= limit) continue;
|
|
181
|
+
let row: any;
|
|
182
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
183
|
+
if (row.type !== "match") continue;
|
|
184
|
+
hits.push({
|
|
185
|
+
file: String(row.data.path.text).replace(HOME, "~"),
|
|
186
|
+
line: row.data.line_number,
|
|
187
|
+
text: String(row.data.lines.text).trim().slice(0, 400),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
resolve(hits);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── server ───────────────────────────────────────────────────────────────────
|
|
196
|
+
const server = Bun.serve({
|
|
197
|
+
port: PORT,
|
|
198
|
+
hostname: HOST,
|
|
199
|
+
async fetch(request) {
|
|
200
|
+
const id = randomUUID().slice(0, 8);
|
|
201
|
+
const url = new URL(request.url);
|
|
202
|
+
const { pathname } = url;
|
|
203
|
+
const method = request.method;
|
|
204
|
+
const base = { id, at: new Date().toISOString(), path: pathname, method };
|
|
205
|
+
|
|
206
|
+
// liveness probe needs no token, and reveals nothing but shape
|
|
207
|
+
if (method === "GET" && pathname === "/health") {
|
|
208
|
+
return json(200, {
|
|
209
|
+
ok: true,
|
|
210
|
+
readOnly: true,
|
|
211
|
+
routes: [...Object.keys(FORWARD_GET), ...Object.keys(FORWARD_POST),
|
|
212
|
+
...(FILE_SEARCH_ENABLED ? ["/files/search"] : [])],
|
|
213
|
+
fileScopes: Object.keys(FILE_ROOTS),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!authOk(request.headers.get("authorization"))) {
|
|
218
|
+
log({ ...base, r: "auth" });
|
|
219
|
+
return json(401, { error: "unauthorized" });
|
|
220
|
+
}
|
|
221
|
+
if (!rateOk()) {
|
|
222
|
+
log({ ...base, r: "rate" });
|
|
223
|
+
return json(429, { error: `rate limited, ${RATE_MAX}/min` });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
if (method === "GET" && FORWARD_GET[pathname]) {
|
|
228
|
+
log({ ...base, r: "fwd" });
|
|
229
|
+
return await forward(FORWARD_GET[pathname]!, "GET");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (method === "POST" && FORWARD_POST[pathname]) {
|
|
233
|
+
const raw = await request.text();
|
|
234
|
+
if (raw.length > MAX_REQUEST_BYTES) {
|
|
235
|
+
log({ ...base, r: "too-large" });
|
|
236
|
+
return json(413, { error: `request body exceeds ${MAX_REQUEST_BYTES} bytes` });
|
|
237
|
+
}
|
|
238
|
+
try { JSON.parse(raw || "{}"); } catch {
|
|
239
|
+
return json(400, { error: "body must be valid JSON" });
|
|
240
|
+
}
|
|
241
|
+
log({ ...base, r: "fwd" });
|
|
242
|
+
return await forward(FORWARD_POST[pathname]!, "POST", raw || "{}");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (method === "GET" && pathname === "/files/search") {
|
|
246
|
+
if (!FILE_SEARCH_ENABLED) return json(404, { error: "file search not configured" });
|
|
247
|
+
const q = (url.searchParams.get("q") || "").trim();
|
|
248
|
+
const scope = url.searchParams.get("scope") || "all";
|
|
249
|
+
const limit = Math.min(clampInt(url.searchParams.get("limit") ?? undefined, 10, 1, 30), 30);
|
|
250
|
+
if (q.length < 2 || q.length > 200) return json(400, { error: "q must be 2-200 chars" });
|
|
251
|
+
if (scope !== "all" && !FILE_ROOTS[scope]) {
|
|
252
|
+
return json(400, { error: `scope must be all|${Object.keys(FILE_ROOTS).join("|")}` });
|
|
253
|
+
}
|
|
254
|
+
const hits = await fileSearch(q, scope, limit);
|
|
255
|
+
log({ ...base, q, scope, r: "ok", n: hits.length });
|
|
256
|
+
return json(200, { query: q, scope, count: hits.length, hits });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
log({ ...base, r: "404" });
|
|
260
|
+
return json(404, { error: "not found (this gateway is read-only)" });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
263
|
+
log({ ...base, r: "err", msg: message });
|
|
264
|
+
return json(502, { error: "gateway error", detail: message });
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
console.log(
|
|
270
|
+
`RecallNest read-only gateway on http://${HOST}:${server.port} → ${API_URL}` +
|
|
271
|
+
(FILE_SEARCH_ENABLED ? ` | file scopes: ${Object.keys(FILE_ROOTS).join(", ")}` : ""),
|
|
272
|
+
);
|