freshcontext-mcp 0.3.16 → 0.3.18
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/.env.example +3 -0
- package/LICENSE +21 -0
- package/NOTICE.md +17 -0
- package/README.md +395 -296
- package/SECURITY.md +34 -0
- package/TRADEMARKS.md +9 -0
- package/dist/adapters/arxiv.js +92 -48
- package/dist/adapters/finance.js +87 -101
- package/dist/adapters/gdelt.js +1 -1
- package/dist/adapters/gebiz.js +1 -1
- package/dist/adapters/hackernews.js +59 -29
- package/dist/adapters/productHunt.js +8 -4
- package/dist/adapters/registry.js +232 -0
- package/dist/adapters/repoSearch.js +1 -1
- package/dist/adapters/secFilings.js +1 -1
- package/dist/core/decay.js +61 -0
- package/dist/core/decision.js +176 -0
- package/dist/core/envelope.js +59 -0
- package/dist/core/explain.js +28 -0
- package/dist/core/guards.js +17 -0
- package/dist/core/index.js +11 -0
- package/dist/core/pipeline.js +101 -0
- package/dist/core/provenance.js +73 -0
- package/dist/core/rank.js +84 -0
- package/dist/core/signal.js +101 -0
- package/dist/core/sourceProfiles.js +126 -0
- package/dist/core/types.js +1 -0
- package/dist/core/utility.js +90 -0
- package/dist/rest/handler.js +126 -0
- package/dist/security.js +1 -1
- package/dist/server.js +10 -10
- package/dist/tools/freshnessStamp.js +1 -117
- package/dist/types.js +0 -1
- package/docs/API_DESIGN.md +434 -0
- package/docs/CODEX_MCP_USAGE.md +116 -0
- package/docs/CORE_API.md +224 -0
- package/docs/DEPENDENCY_DILIGENCE.md +63 -0
- package/docs/HA_PRI_V2_DESIGN.md +279 -0
- package/docs/OPERATIONAL_DEMO_RUNBOOK.md +458 -0
- package/docs/RELEASE_INTEGRITY.md +53 -0
- package/docs/RELEASE_NOTES.md +38 -0
- package/docs/SIGNAL_CONTRACT.md +89 -0
- package/docs/SOURCE_PROFILES.md +427 -0
- package/freshcontext.schema.json +103 -103
- package/package-script-guard.mjs +140 -0
- package/package.json +92 -52
- package/server.json +27 -28
- package/.github/workflows/publish.yml +0 -32
- package/RESEARCH.md +0 -487
- package/RISKS.md +0 -137
- package/cleanup.ps1 +0 -99
- package/demo/README.md +0 -70
- package/demo/data.json +0 -88
- package/demo/generate.mjs +0 -199
- package/demo/index.html +0 -513
- package/demo/logo-export.html +0 -61
- package/demo/logo.svg +0 -23
- package/dist/apify.js +0 -133
- package/freshcontext-validate.js +0 -196
- package/time-check.ps1 +0 -46
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { evaluateSignal, evaluateSignals } from "../core/index.js";
|
|
2
|
+
const SERVICE_VERSION = "0.1.0";
|
|
3
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
4
|
+
const MAX_BODY_BYTES = 256 * 1024;
|
|
5
|
+
function jsonResponse(body, status = 200) {
|
|
6
|
+
return new Response(JSON.stringify(body), {
|
|
7
|
+
status,
|
|
8
|
+
headers: { "Content-Type": JSON_CONTENT_TYPE },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function errorResponse(code, message, status, details = []) {
|
|
12
|
+
return jsonResponse({ error: { code, message, details } }, status);
|
|
13
|
+
}
|
|
14
|
+
function methodNotAllowed(allowed) {
|
|
15
|
+
return new Response(JSON.stringify({
|
|
16
|
+
error: {
|
|
17
|
+
code: "method_not_allowed",
|
|
18
|
+
message: `Method not allowed. Use ${allowed}.`,
|
|
19
|
+
details: [],
|
|
20
|
+
},
|
|
21
|
+
}), {
|
|
22
|
+
status: 405,
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": JSON_CONTENT_TYPE,
|
|
25
|
+
"Allow": allowed,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function isJsonContentType(request) {
|
|
30
|
+
return (request.headers.get("Content-Type") ?? "").toLowerCase().includes(JSON_CONTENT_TYPE);
|
|
31
|
+
}
|
|
32
|
+
function isRecord(value) {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
async function readJsonBody(request) {
|
|
36
|
+
if (!isJsonContentType(request)) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
response: errorResponse("unsupported_media_type", "POST requests require Content-Type: application/json.", 415),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const contentLength = request.headers.get("Content-Length");
|
|
43
|
+
if (contentLength !== null && Number(contentLength) > MAX_BODY_BYTES) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
response: errorResponse("payload_too_large", `Request body exceeds ${MAX_BODY_BYTES} bytes.`, 413),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const text = await request.text();
|
|
50
|
+
if (new TextEncoder().encode(text).length > MAX_BODY_BYTES) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
response: errorResponse("payload_too_large", `Request body exceeds ${MAX_BODY_BYTES} bytes.`, 413),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(text);
|
|
58
|
+
if (!isRecord(parsed)) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
response: errorResponse("invalid_request", "Request body must be a JSON object.", 400),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, body: parsed };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
response: errorResponse("invalid_request", "Request body must be valid JSON.", 400),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function optionsFromBody(body) {
|
|
74
|
+
if (body.options === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
return isRecord(body.options) ? body.options : {};
|
|
77
|
+
}
|
|
78
|
+
async function handleEvaluate(request) {
|
|
79
|
+
if (request.method !== "POST")
|
|
80
|
+
return methodNotAllowed("POST");
|
|
81
|
+
const parsed = await readJsonBody(request);
|
|
82
|
+
if (!parsed.ok)
|
|
83
|
+
return parsed.response;
|
|
84
|
+
if (!isRecord(parsed.body.signal)) {
|
|
85
|
+
return errorResponse("invalid_request", "Request body must include signal.", 400);
|
|
86
|
+
}
|
|
87
|
+
const result = evaluateSignal(parsed.body.signal, optionsFromBody(parsed.body));
|
|
88
|
+
return jsonResponse(result);
|
|
89
|
+
}
|
|
90
|
+
async function handleEvaluateBatch(request) {
|
|
91
|
+
if (request.method !== "POST")
|
|
92
|
+
return methodNotAllowed("POST");
|
|
93
|
+
const parsed = await readJsonBody(request);
|
|
94
|
+
if (!parsed.ok)
|
|
95
|
+
return parsed.response;
|
|
96
|
+
if (!Array.isArray(parsed.body.signals)) {
|
|
97
|
+
return errorResponse("invalid_request", "Request body must include signals array.", 400);
|
|
98
|
+
}
|
|
99
|
+
const result = evaluateSignals(parsed.body.signals, optionsFromBody(parsed.body));
|
|
100
|
+
return jsonResponse({ evaluations: result });
|
|
101
|
+
}
|
|
102
|
+
function handleHealth(request) {
|
|
103
|
+
if (request.method !== "GET")
|
|
104
|
+
return methodNotAllowed("GET");
|
|
105
|
+
return jsonResponse({
|
|
106
|
+
ok: true,
|
|
107
|
+
service: "freshcontext-rest",
|
|
108
|
+
version: SERVICE_VERSION,
|
|
109
|
+
core_available: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export async function handleRestRequest(request) {
|
|
113
|
+
const url = new URL(request.url);
|
|
114
|
+
try {
|
|
115
|
+
if (url.pathname === "/v1/health")
|
|
116
|
+
return handleHealth(request);
|
|
117
|
+
if (url.pathname === "/v1/evaluate")
|
|
118
|
+
return handleEvaluate(request);
|
|
119
|
+
if (url.pathname === "/v1/evaluate-batch")
|
|
120
|
+
return handleEvaluateBatch(request);
|
|
121
|
+
return errorResponse("not_found", `Not found: ${url.pathname}.`, 404);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return errorResponse("internal_error", "Unexpected REST host error.", 500);
|
|
125
|
+
}
|
|
126
|
+
}
|
package/dist/security.js
CHANGED
|
@@ -11,7 +11,7 @@ export const ALLOWED_DOMAINS = {
|
|
|
11
11
|
repoSearch: [], // uses GitHub API directly, no browser
|
|
12
12
|
packageTrends: [], // uses npm/PyPI APIs directly, no browser
|
|
13
13
|
reddit: [], // uses public Reddit JSON API, no browser
|
|
14
|
-
finance: [], // uses
|
|
14
|
+
finance: [], // uses Stooq quote API, no browser
|
|
15
15
|
productHunt: ["www.producthunt.com", "producthunt.com"],
|
|
16
16
|
};
|
|
17
17
|
// ─── Blocked IP ranges and internal hostnames ────────────────────────────────
|
package/dist/server.js
CHANGED
|
@@ -22,7 +22,7 @@ import { stampFreshness, formatForLLM } from "./tools/freshnessStamp.js";
|
|
|
22
22
|
import { formatSecurityError } from "./security.js";
|
|
23
23
|
const server = new McpServer({
|
|
24
24
|
name: "freshcontext-mcp",
|
|
25
|
-
version: "0.
|
|
25
|
+
version: "0.3.18",
|
|
26
26
|
});
|
|
27
27
|
// ─── Tool: extract_github ────────────────────────────────────────────────────
|
|
28
28
|
server.registerTool("extract_github", {
|
|
@@ -62,9 +62,9 @@ server.registerTool("extract_scholar", {
|
|
|
62
62
|
});
|
|
63
63
|
// ─── Tool: extract_hackernews ────────────────────────────────────────────────
|
|
64
64
|
server.registerTool("extract_hackernews", {
|
|
65
|
-
description: "Extract top stories or search results from Hacker News.
|
|
65
|
+
description: "Extract top stories or search results from Hacker News. Accepts an HN/Algolia URL or a plain search query while preserving the url field for compatibility.",
|
|
66
66
|
inputSchema: z.object({
|
|
67
|
-
url: z.string().
|
|
67
|
+
url: z.string().min(1).describe("HN URL e.g. https://news.ycombinator.com/news, Algolia API URL, or search query e.g. 'browser agents'"),
|
|
68
68
|
max_length: z.number().optional().default(4000),
|
|
69
69
|
}),
|
|
70
70
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
@@ -152,7 +152,7 @@ server.registerTool("extract_arxiv", {
|
|
|
152
152
|
});
|
|
153
153
|
// ─── Tool: extract_finance ───────────────────────────────────────────────────
|
|
154
154
|
server.registerTool("extract_finance", {
|
|
155
|
-
description: "
|
|
155
|
+
description: "No-key stock quote data via Stooq — close, open, high, low, volume, quote timestamp, and source. Accepts up to 5 comma-separated tickers. Returns timestamped freshcontext only for successful observations.",
|
|
156
156
|
inputSchema: z.object({
|
|
157
157
|
url: z.string().describe("Ticker symbol(s) e.g. 'AAPL' or 'MSFT,GOOG,PLTR'"),
|
|
158
158
|
max_length: z.number().optional().default(5000),
|
|
@@ -355,7 +355,7 @@ server.registerTool("extract_gov_landscape", {
|
|
|
355
355
|
// community sentiment, repo ecosystem size, and product release velocity.
|
|
356
356
|
// Unique: Bloomberg Terminal doesn't read commit history as a company health signal.
|
|
357
357
|
server.registerTool("extract_finance_landscape", {
|
|
358
|
-
description: "Composite financial intelligence tool for developers. Given one or more ticker symbols, simultaneously queries: (1)
|
|
358
|
+
description: "Composite financial intelligence tool for developers. Given one or more ticker symbols, simultaneously queries: (1) Stooq for no-key quote data, (2) Hacker News for developer community sentiment, (3) Reddit for investor and tech community discussion, (4) GitHub for repo ecosystem activity around the company's tech, and (5) their product changelog for release velocity as a company health signal. Answers: What's the price? What are developers saying? Is the company actually shipping? Returns a unified 5-source timestamped report.",
|
|
359
359
|
inputSchema: z.object({
|
|
360
360
|
tickers: z.string().describe("One or more ticker symbols e.g. 'PLTR' or 'PLTR,MSFT,GOOG'. Up to 5 tickers."),
|
|
361
361
|
company_name: z.string().optional().describe("Company name for HN/Reddit/GitHub searches e.g. 'Palantir'. If omitted, derived from the ticker."),
|
|
@@ -381,10 +381,10 @@ server.registerTool("extract_finance_landscape", {
|
|
|
381
381
|
const combined = [
|
|
382
382
|
`# Finance + Developer Intelligence: "${tickers}"${company_name ? ` (${company_name})` : ""}`,
|
|
383
383
|
`Generated: ${new Date().toISOString()}`,
|
|
384
|
-
`Sources:
|
|
384
|
+
`Sources: Stooq · Hacker News · Reddit · GitHub · Changelog`,
|
|
385
385
|
min_freshness_score ? `min_freshness_score: ${min_freshness_score}` : null,
|
|
386
386
|
"",
|
|
387
|
-
sectionWithFreshnessCheck("📈 Market Data (
|
|
387
|
+
sectionWithFreshnessCheck("📈 Market Data (Stooq)", priceResult, "finance", min_freshness_score),
|
|
388
388
|
sectionWithFreshnessCheck("💬 Developer Sentiment (Hacker News)", hnResult, "hackernews", min_freshness_score),
|
|
389
389
|
sectionWithFreshnessCheck("🗣️ Community Discussion (Reddit)", redditResult, "reddit", min_freshness_score),
|
|
390
390
|
sectionWithFreshnessCheck("📦 Repo Ecosystem (GitHub)", repoResult, "reposearch", min_freshness_score),
|
|
@@ -442,7 +442,7 @@ server.registerTool("extract_gdelt", {
|
|
|
442
442
|
// global news intelligence + product release velocity + market pricing.
|
|
443
443
|
// Unique: this combination exists nowhere else.
|
|
444
444
|
server.registerTool("extract_company_landscape", {
|
|
445
|
-
description: "Composite company intelligence tool. The most complete single-call company analysis available. Simultaneously queries 5 unique sources: (1) SEC EDGAR for 8-K material event filings — what the company legally just disclosed, (2) USASpending.gov for federal contract footprint — who is giving them government money, (3) GDELT for global news intelligence — what the world is saying about them right now, (4) their product changelog — are they actually shipping, (5)
|
|
445
|
+
description: "Composite company intelligence tool. The most complete single-call company analysis available. Simultaneously queries 5 unique sources: (1) SEC EDGAR for 8-K material event filings — what the company legally just disclosed, (2) USASpending.gov for federal contract footprint — who is giving them government money, (3) GDELT for global news intelligence — what the world is saying about them right now, (4) their product changelog — are they actually shipping, (5) Stooq quote data — what the market is pricing in. Returns a unified 5-source timestamped report. Unique: this combination is not available in any other MCP server.",
|
|
446
446
|
inputSchema: z.object({
|
|
447
447
|
company: z.string().describe("Company name e.g. 'Palantir', 'Anthropic', 'OpenAI'"),
|
|
448
448
|
ticker: z.string().optional().describe("Stock ticker for finance data e.g. 'PLTR'. Leave blank for private companies."),
|
|
@@ -464,14 +464,14 @@ server.registerTool("extract_company_landscape", {
|
|
|
464
464
|
const combined = [
|
|
465
465
|
`# Company Intelligence Landscape: "${company}"${ticker ? ` (${ticker})` : ""}`,
|
|
466
466
|
`Generated: ${new Date().toISOString()}`,
|
|
467
|
-
`Sources: SEC EDGAR · USASpending.gov · GDELT · Changelog ·
|
|
467
|
+
`Sources: SEC EDGAR · USASpending.gov · GDELT · Changelog · Stooq`,
|
|
468
468
|
min_freshness_score ? `min_freshness_score: ${min_freshness_score}` : null,
|
|
469
469
|
"",
|
|
470
470
|
sectionWithFreshnessCheck("📋 SEC 8-K Filings — Legal Disclosures", secResult, "sec_filings", min_freshness_score),
|
|
471
471
|
sectionWithFreshnessCheck("🏛️ Federal Contract Awards (USASpending.gov)", contractsResult, "govcontracts", min_freshness_score),
|
|
472
472
|
sectionWithFreshnessCheck("🌍 Global News Intelligence (GDELT)", gdeltResult, "gdelt", min_freshness_score),
|
|
473
473
|
sectionWithFreshnessCheck("🔄 Product Release Velocity (Changelog)", changelogResult, "changelog", min_freshness_score),
|
|
474
|
-
sectionWithFreshnessCheck("📈 Market Data (
|
|
474
|
+
sectionWithFreshnessCheck("📈 Market Data (Stooq)", financeResult, "finance", min_freshness_score),
|
|
475
475
|
].filter(Boolean).join("\n\n");
|
|
476
476
|
return { content: [{ type: "text", text: combined }] };
|
|
477
477
|
});
|
|
@@ -1,117 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// Spec-compliant exponential DAR model.
|
|
3
|
-
// Higher lambda = data goes stale faster. Half-life formula: t½ = ln(2) / λ.
|
|
4
|
-
// Lambda is measured per hour and mirrors the Worker/D1 intelligence engine.
|
|
5
|
-
export const LAMBDA = {
|
|
6
|
-
hackernews: 0.050,
|
|
7
|
-
reddit: 0.010,
|
|
8
|
-
producthunt: 0.010,
|
|
9
|
-
jobs: 0.005,
|
|
10
|
-
finance: 0.001,
|
|
11
|
-
yc: 0.001,
|
|
12
|
-
packagetrends: 0.0005,
|
|
13
|
-
github: 0.0002,
|
|
14
|
-
reposearch: 0.0002,
|
|
15
|
-
google_scholar: 0.00005,
|
|
16
|
-
arxiv: 0.00005,
|
|
17
|
-
changelog: 0.0005,
|
|
18
|
-
gdelt: 0.020,
|
|
19
|
-
gebiz: 0.003,
|
|
20
|
-
govcontracts: 0.001,
|
|
21
|
-
sec_filings: 0.005,
|
|
22
|
-
landscape: 0.050,
|
|
23
|
-
gov_landscape: 0.001,
|
|
24
|
-
finance_landscape: 0.001,
|
|
25
|
-
company_landscape: 0.005,
|
|
26
|
-
idea_landscape: 0.050,
|
|
27
|
-
default: 0.001,
|
|
28
|
-
};
|
|
29
|
-
// ─── Score calculation ────────────────────────────────────────────────────────
|
|
30
|
-
// Returns null when content_date is unknown — we can't calculate age without a date.
|
|
31
|
-
// Returns a clamped 0-100 exponential freshness score.
|
|
32
|
-
function calculateFreshnessScore(content_date, retrieved_at, adapter) {
|
|
33
|
-
if (!content_date)
|
|
34
|
-
return null;
|
|
35
|
-
const published = new Date(content_date).getTime();
|
|
36
|
-
const retrieved = new Date(retrieved_at).getTime();
|
|
37
|
-
// Guard against unparseable dates
|
|
38
|
-
if (isNaN(published) || isNaN(retrieved))
|
|
39
|
-
return null;
|
|
40
|
-
const hoursSinceRetrieved = Math.max(0, (retrieved - published) / (1000 * 60 * 60));
|
|
41
|
-
const lambda = LAMBDA[adapter] ?? LAMBDA.default;
|
|
42
|
-
return Math.max(0, Math.round(100 * Math.exp(-lambda * hoursSinceRetrieved)));
|
|
43
|
-
}
|
|
44
|
-
// ─── Score label ──────────────────────────────────────────────────────────────
|
|
45
|
-
// Human-readable interpretation alongside the number, per the spec.
|
|
46
|
-
function scoreLabel(score) {
|
|
47
|
-
if (score === null)
|
|
48
|
-
return "unknown";
|
|
49
|
-
if (score >= 90)
|
|
50
|
-
return "current";
|
|
51
|
-
if (score >= 70)
|
|
52
|
-
return "reliable";
|
|
53
|
-
if (score >= 50)
|
|
54
|
-
return "verify before acting";
|
|
55
|
-
return "use with caution";
|
|
56
|
-
}
|
|
57
|
-
// ─── Main stamp function ──────────────────────────────────────────────────────
|
|
58
|
-
export function stampFreshness(result, options, adapter) {
|
|
59
|
-
const retrieved_at = new Date().toISOString();
|
|
60
|
-
const freshness_score = calculateFreshnessScore(result.content_date, retrieved_at, adapter);
|
|
61
|
-
return {
|
|
62
|
-
content: result.raw.slice(0, options.maxLength ?? 8000),
|
|
63
|
-
source_url: options.url,
|
|
64
|
-
content_date: result.content_date,
|
|
65
|
-
retrieved_at,
|
|
66
|
-
freshness_confidence: result.freshness_confidence,
|
|
67
|
-
freshness_score,
|
|
68
|
-
adapter,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
// ─── Structured JSON form ─────────────────────────────────────────────────────
|
|
72
|
-
// Returns the spec-compliant JSON object defined in FRESHCONTEXT_SPEC.md.
|
|
73
|
-
// Programmatic consumers can parse this without touching the text envelope.
|
|
74
|
-
export function toStructuredJSON(ctx) {
|
|
75
|
-
return {
|
|
76
|
-
freshcontext: {
|
|
77
|
-
source_url: ctx.source_url,
|
|
78
|
-
content_date: ctx.content_date,
|
|
79
|
-
retrieved_at: ctx.retrieved_at,
|
|
80
|
-
freshness_confidence: ctx.freshness_confidence,
|
|
81
|
-
freshness_score: ctx.freshness_score,
|
|
82
|
-
adapter: ctx.adapter,
|
|
83
|
-
},
|
|
84
|
-
content: ctx.content,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
// ─── Text envelope formatter ──────────────────────────────────────────────────
|
|
88
|
-
// Produces the [FRESHCONTEXT]...[/FRESHCONTEXT] envelope defined in the spec,
|
|
89
|
-
// followed by a [FRESHCONTEXT_JSON]...[/FRESHCONTEXT_JSON] block so both the
|
|
90
|
-
// human-readable envelope and the machine-parseable JSON travel together.
|
|
91
|
-
export function formatForLLM(ctx) {
|
|
92
|
-
const dateInfo = ctx.content_date
|
|
93
|
-
? `Published: ${ctx.content_date}`
|
|
94
|
-
: "Publish date: unknown";
|
|
95
|
-
const scoreLine = ctx.freshness_score !== null
|
|
96
|
-
? `Score: ${ctx.freshness_score}/100 (${scoreLabel(ctx.freshness_score)})`
|
|
97
|
-
: `Score: unknown`;
|
|
98
|
-
const textEnvelope = [
|
|
99
|
-
`[FRESHCONTEXT]`,
|
|
100
|
-
`Source: ${ctx.source_url}`,
|
|
101
|
-
`${dateInfo}`,
|
|
102
|
-
`Retrieved: ${ctx.retrieved_at}`,
|
|
103
|
-
`Confidence: ${ctx.freshness_confidence}`,
|
|
104
|
-
`${scoreLine}`,
|
|
105
|
-
`---`,
|
|
106
|
-
ctx.content,
|
|
107
|
-
`[/FRESHCONTEXT]`,
|
|
108
|
-
].join("\n");
|
|
109
|
-
// Append the structured JSON block so programmatic consumers
|
|
110
|
-
// can extract metadata without parsing the text envelope.
|
|
111
|
-
const jsonBlock = [
|
|
112
|
-
`[FRESHCONTEXT_JSON]`,
|
|
113
|
-
JSON.stringify(toStructuredJSON(ctx), null, 2),
|
|
114
|
-
`[/FRESHCONTEXT_JSON]`,
|
|
115
|
-
].join("\n");
|
|
116
|
-
return `${textEnvelope}\n\n${jsonBlock}`;
|
|
117
|
-
}
|
|
1
|
+
export { LAMBDA, stampFreshness, toStructuredJSON, formatForLLM, } from "../core/index.js";
|
package/dist/types.js
CHANGED