@plasm_lang/vercel-agent 0.3.78 → 0.3.81

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 (49) hide show
  1. package/agent/instructions.md +1 -2
  2. package/bin/plasm-agent.mjs +5 -7
  3. package/package.json +26 -19
  4. package/scripts/plasm-cli.ts +4 -3
  5. package/scripts/run-plasm-cli.mjs +41 -0
  6. package/src/cli/init-scaffold.ts +198 -0
  7. package/src/cli/init.ts +16 -278
  8. package/src/cli/nitro-dev.ts +2 -6
  9. package/src/cli/node-dev-imports.ts +14 -0
  10. package/src/package-version.ts +14 -0
  11. package/src/project-info.ts +2 -1
  12. package/templates/mcp-radar/.vercelignore +3 -0
  13. package/templates/mcp-radar/README.md +139 -0
  14. package/templates/mcp-radar/agent/agent.ts +38 -0
  15. package/templates/mcp-radar/agent/catalogs/hackernews/README.md +23 -0
  16. package/templates/mcp-radar/agent/catalogs/hackernews/domain.yaml +329 -0
  17. package/templates/mcp-radar/agent/catalogs/hackernews/eval/cases.yaml +112 -0
  18. package/templates/mcp-radar/agent/catalogs/hackernews/mappings.yaml +129 -0
  19. package/templates/mcp-radar/agent/catalogs/tavily/README.md +218 -0
  20. package/templates/mcp-radar/agent/catalogs/tavily/domain.yaml +373 -0
  21. package/templates/mcp-radar/agent/catalogs/tavily/eval/cases.yaml +56 -0
  22. package/templates/mcp-radar/agent/catalogs/tavily/eval/google-gemma-3-4b-it.latest.human.txt +67 -0
  23. package/templates/mcp-radar/agent/catalogs/tavily/eval/google-gemma-3-4b-it.latest.json +363 -0
  24. package/templates/mcp-radar/agent/catalogs/tavily/mappings.yaml +171 -0
  25. package/templates/mcp-radar/agent/channels/mcp-radar.ts +85 -0
  26. package/templates/mcp-radar/agent/hooks/proof-audit.ts +10 -0
  27. package/templates/mcp-radar/agent/instructions.md +24 -0
  28. package/templates/mcp-radar/agent/research/mcp-innovations-proof.md +4 -0
  29. package/templates/mcp-radar/agent/schedules/mcp-radar-scan.ts +14 -0
  30. package/templates/mcp-radar/agent/skills/mcp-proof-format.md +19 -0
  31. package/templates/mcp-radar/api/[[...path]].ts +23 -0
  32. package/templates/mcp-radar/evals/mcp-radar-discover.eval.ts +14 -0
  33. package/templates/mcp-radar/lib/proof-store.ts +265 -0
  34. package/templates/mcp-radar/lib/run-radar.ts +214 -0
  35. package/templates/mcp-radar/nitro.config.ts +17 -0
  36. package/templates/mcp-radar/package.json +14 -0
  37. package/templates/mcp-radar/public/index.html +10 -0
  38. package/templates/mcp-radar/routes/[...path].ts +29 -0
  39. package/templates/mcp-radar/scripts/run-evals.ts +46 -0
  40. package/templates/mcp-radar/scripts/smoke-channel.ts +66 -0
  41. package/templates/mcp-radar/vercel.json +15 -0
  42. package/templates/scaffold/.vercelignore +3 -0
  43. package/templates/scaffold/api/[[...path]].ts +23 -0
  44. package/templates/scaffold/nitro.config.ts +17 -0
  45. package/templates/scaffold/public/index.html +10 -0
  46. package/templates/scaffold/public/index.mcp-radar.html +10 -0
  47. package/templates/scaffold/routes/[...path].ts +29 -0
  48. package/templates/scaffold/vercel.default.json +4 -0
  49. package/templates/scaffold/vercel.mcp-radar.json +15 -0
@@ -0,0 +1,10 @@
1
+ import { defineHook } from "@plasm_lang/vercel-agent";
2
+
3
+ export default defineHook({
4
+ name: "proof-audit",
5
+ on: ["run:complete", "plan:commit"],
6
+ handler: (_ctx, detail) => {
7
+ const keys = detail && typeof detail === "object" ? Object.keys(detail) : [];
8
+ console.log(`[mcp-radar:hook:proof-audit] event detail keys=${keys.join(",")}`);
9
+ },
10
+ });
@@ -0,0 +1,24 @@
1
+ # MCP Radar
2
+
3
+ You track **Model Context Protocol (MCP)** innovation signals on Hacker News and corroborate them with Tavily web search.
4
+
5
+ ## Tool order
6
+
7
+ 1. **`discover_capabilities`** — only when unsure which catalog entities to use.
8
+ 2. **`plasm_context`** — open or extend a session with seeds:
9
+ - `hackernews:Item`
10
+ - `tavily:SearchResult`
11
+ - Use stable intent: `track MCP innovations from Hacker News and corroborate with Tavily web search`
12
+ 3. **`plasm`** — dry-run programs using teaching TSV symbols.
13
+ 4. **`plasm_run`** — live execute reviewed plans (`pcN` only).
14
+
15
+ ## Per-run workflow
16
+
17
+ 1. Search HN (`item_search` or `item_search_by_date`) for MCP-related stories from the candidate list in the user goal.
18
+ 2. For each **new** story, optionally `item_get` for score and metadata.
19
+ 3. If Tavily is available, run `web_search` with a query derived from the story title to corroborate.
20
+ 4. Emit **proof entries** using the `mcp-proof-format` skill template (one `###` block per story).
21
+
22
+ If Tavily is unavailable, synthesize from HN only and set **Confidence: low**.
23
+
24
+ Do not invent `e#` / `p#` symbols — copy from the teaching TSV.
@@ -0,0 +1,4 @@
1
+ # MCP Innovations Proof Log
2
+
3
+ Append-only research artifact maintained by the MCP Radar agent.
4
+ Each automated or manual run adds a dated section below.
@@ -0,0 +1,14 @@
1
+ import { defineSchedule } from "@plasm_lang/vercel-agent";
2
+
3
+ import { runRadar } from "../../lib/run-radar.js";
4
+
5
+ export default defineSchedule({
6
+ name: "mcp-radar-scan",
7
+ cron: "0 */6 * * *",
8
+ handler: async (ctx) => {
9
+ const result = await runRadar(ctx);
10
+ console.log(
11
+ `[mcp-radar:schedule] ok=${result.ok} skipped=${result.skipped} new=${result.newCandidates.length} reason=${result.reason ?? ""}`,
12
+ );
13
+ },
14
+ });
@@ -0,0 +1,19 @@
1
+ # MCP proof entry format
2
+
3
+ When the user asks for proof entries, output markdown blocks exactly in this shape (one per story):
4
+
5
+ ```markdown
6
+ ### [Story title](hn-url)
7
+ - **HN**: id {id}, score {score}, posted {iso-or-relative}
8
+ - **Tavily**: {source title} ({url}); {optional second source}
9
+ - **Synthesis**: 2–3 sentences on what MCP innovation angle this represents.
10
+ - **Confidence**: high | medium | low
11
+ ```
12
+
13
+ ## Correlation rubric
14
+
15
+ - **high**: Tavily finds independent coverage of the same product/announcement within 7 days.
16
+ - **medium**: Tavily finds related MCP ecosystem context but not the exact story.
17
+ - **low**: HN only, or Tavily unavailable, or weak keyword match.
18
+
19
+ Skip stories that are not meaningfully about MCP (Model Context Protocol), unless the run goal explicitly lists them.
@@ -0,0 +1,23 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import agentDefinition from "../agent/agent.js";
5
+ import { createPlasmApp, vercelPlasmHandler } from "@plasm_lang/vercel-agent";
6
+
7
+ const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
8
+ const agentRoot = path.join(packageRoot, "agent");
9
+
10
+ let app: Awaited<ReturnType<typeof createPlasmApp>> | undefined;
11
+
12
+ export default async function handler(
13
+ req: import("node:http").IncomingMessage,
14
+ res: import("node:http").ServerResponse,
15
+ ): Promise<void> {
16
+ app ??= await createPlasmApp({
17
+ agentRoot,
18
+ definition: agentDefinition,
19
+ mode: "prod",
20
+ sessions: false,
21
+ });
22
+ await vercelPlasmHandler(app)(req, res);
23
+ }
@@ -0,0 +1,14 @@
1
+ import { defineEval } from "@plasm_lang/vercel-agent";
2
+
3
+ export default defineEval({
4
+ name: "mcp-radar-discover",
5
+ goal: [
6
+ "Search Hacker News for recent discussion about MCP (Model Context Protocol).",
7
+ "Open a federated plasm session for hackernews and tavily if available.",
8
+ "Summarize one interesting story and what MCP innovation it represents.",
9
+ ].join(" "),
10
+ assert: {
11
+ toolsUsedAny: ["plasm_context", "plasm_run"],
12
+ minSteps: 1,
13
+ },
14
+ });
@@ -0,0 +1,265 @@
1
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export interface SeenState {
5
+ itemIds: string[];
6
+ lastRunAt?: string;
7
+ lastRunStatus?: "ok" | "skipped" | "error";
8
+ lastNewCount?: number;
9
+ }
10
+
11
+ export interface LastRunMeta {
12
+ at: string;
13
+ status: "ok" | "skipped" | "error";
14
+ newItems: number;
15
+ message?: string;
16
+ }
17
+
18
+ export type ProofStoreBackend = "fs" | "vercel";
19
+
20
+ export interface ProofStore {
21
+ backend(): ProofStoreBackend;
22
+ readProofMarkdown(): Promise<string>;
23
+ appendProofRun(section: { runAt: string; body: string }): Promise<void>;
24
+ loadSeenState(): Promise<SeenState>;
25
+ saveSeenState(state: SeenState): Promise<void>;
26
+ addSeenIds(ids: string[]): Promise<SeenState>;
27
+ loadLastRun(): Promise<LastRunMeta | null>;
28
+ saveLastRun(meta: LastRunMeta): Promise<void>;
29
+ }
30
+
31
+ const SEEN_KV_KEY = "plasm:mcp-radar:seen";
32
+ const LAST_RUN_KV_KEY = "plasm:mcp-radar:last-run";
33
+ const PROOF_BLOB_KEY = "research/mcp-innovations-proof.md";
34
+ const PROOF_HEADER = "# MCP Innovations Proof Log\n\n";
35
+
36
+ function researchDir(agentRoot: string): string {
37
+ return path.join(agentRoot, ".plasm", "research");
38
+ }
39
+
40
+ function proofPath(agentRoot: string): string {
41
+ return path.join(agentRoot, "research", "mcp-innovations-proof.md");
42
+ }
43
+
44
+ function seenPath(agentRoot: string): string {
45
+ return path.join(researchDir(agentRoot), "seen-hn-items.json");
46
+ }
47
+
48
+ function lastRunPath(agentRoot: string): string {
49
+ return path.join(researchDir(agentRoot), "last-run.json");
50
+ }
51
+
52
+ export function resolveProofStoreBackend(): ProofStoreBackend {
53
+ const explicit = process.env.PLASM_PROOF_STORE_BACKEND?.trim().toLowerCase();
54
+ if (explicit === "fs" || explicit === "vercel") return explicit;
55
+ const hasKv =
56
+ Boolean(process.env.KV_REST_API_URL?.trim()) ||
57
+ Boolean(process.env.PLASM_KV_REST_API_URL?.trim());
58
+ const hasBlob = Boolean(process.env.BLOB_READ_WRITE_TOKEN?.trim());
59
+ if (hasKv && hasBlob) return "vercel";
60
+ return "fs";
61
+ }
62
+
63
+ type KvClient = {
64
+ get<T>(key: string): Promise<T | null>;
65
+ set(key: string, value: unknown): Promise<unknown>;
66
+ };
67
+
68
+ async function loadKv(): Promise<KvClient> {
69
+ const mod = await import("@vercel/kv");
70
+ return mod.kv as KvClient;
71
+ }
72
+
73
+ async function blobGetText(key: string): Promise<string | null> {
74
+ const { head } = await import("@vercel/blob");
75
+ const meta = await head(key).catch(() => null);
76
+ if (!meta?.url) return null;
77
+ const response = await fetch(meta.url);
78
+ if (!response.ok) return null;
79
+ return response.text();
80
+ }
81
+
82
+ async function blobPutText(key: string, body: string): Promise<void> {
83
+ const { put } = await import("@vercel/blob");
84
+ await put(key, body, { access: "public", addRandomSuffix: false });
85
+ }
86
+
87
+ class FsProofStore implements ProofStore {
88
+ constructor(private readonly agentRoot: string) {}
89
+
90
+ backend(): ProofStoreBackend {
91
+ return "fs";
92
+ }
93
+
94
+ async readProofMarkdown(): Promise<string> {
95
+ try {
96
+ return await readFile(proofPath(this.agentRoot), "utf8");
97
+ } catch {
98
+ return PROOF_HEADER;
99
+ }
100
+ }
101
+
102
+ async appendProofRun(section: { runAt: string; body: string }): Promise<void> {
103
+ const date = section.runAt.slice(0, 10);
104
+ const header = `\n\n## ${date} run (${section.runAt})\n\n`;
105
+ const block = section.body.trim();
106
+ if (!block) return;
107
+ await appendFile(proofPath(this.agentRoot), `${header}${block}\n`, "utf8");
108
+ }
109
+
110
+ async loadSeenState(): Promise<SeenState> {
111
+ try {
112
+ const raw = await readFile(seenPath(this.agentRoot), "utf8");
113
+ const parsed = JSON.parse(raw) as SeenState;
114
+ return {
115
+ itemIds: Array.isArray(parsed.itemIds) ? parsed.itemIds.map(String) : [],
116
+ lastRunAt: parsed.lastRunAt,
117
+ lastRunStatus: parsed.lastRunStatus,
118
+ lastNewCount: parsed.lastNewCount,
119
+ };
120
+ } catch {
121
+ return { itemIds: [] };
122
+ }
123
+ }
124
+
125
+ async saveSeenState(state: SeenState): Promise<void> {
126
+ await mkdir(researchDir(this.agentRoot), { recursive: true });
127
+ await writeFile(seenPath(this.agentRoot), `${JSON.stringify(state, null, 2)}\n`, "utf8");
128
+ }
129
+
130
+ async addSeenIds(ids: string[]): Promise<SeenState> {
131
+ const state = await this.loadSeenState();
132
+ const set = new Set(state.itemIds);
133
+ for (const id of ids) set.add(String(id));
134
+ const next: SeenState = { ...state, itemIds: [...set].sort() };
135
+ await this.saveSeenState(next);
136
+ return next;
137
+ }
138
+
139
+ async loadLastRun(): Promise<LastRunMeta | null> {
140
+ try {
141
+ const raw = await readFile(lastRunPath(this.agentRoot), "utf8");
142
+ return JSON.parse(raw) as LastRunMeta;
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ async saveLastRun(meta: LastRunMeta): Promise<void> {
149
+ await mkdir(researchDir(this.agentRoot), { recursive: true });
150
+ await writeFile(lastRunPath(this.agentRoot), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
151
+ }
152
+ }
153
+
154
+ class VercelProofStore implements ProofStore {
155
+ backend(): ProofStoreBackend {
156
+ return "vercel";
157
+ }
158
+
159
+ async readProofMarkdown(): Promise<string> {
160
+ const text = await blobGetText(PROOF_BLOB_KEY);
161
+ return text ?? PROOF_HEADER;
162
+ }
163
+
164
+ async appendProofRun(section: { runAt: string; body: string }): Promise<void> {
165
+ const date = section.runAt.slice(0, 10);
166
+ const header = `\n\n## ${date} run (${section.runAt})\n\n`;
167
+ const block = section.body.trim();
168
+ if (!block) return;
169
+ const existing = await this.readProofMarkdown();
170
+ await blobPutText(PROOF_BLOB_KEY, `${existing}${header}${block}\n`);
171
+ }
172
+
173
+ async loadSeenState(): Promise<SeenState> {
174
+ const kv = await loadKv();
175
+ const parsed = await kv.get<SeenState>(SEEN_KV_KEY);
176
+ if (!parsed) return { itemIds: [] };
177
+ return {
178
+ itemIds: Array.isArray(parsed.itemIds) ? parsed.itemIds.map(String) : [],
179
+ lastRunAt: parsed.lastRunAt,
180
+ lastRunStatus: parsed.lastRunStatus,
181
+ lastNewCount: parsed.lastNewCount,
182
+ };
183
+ }
184
+
185
+ async saveSeenState(state: SeenState): Promise<void> {
186
+ const kv = await loadKv();
187
+ await kv.set(SEEN_KV_KEY, state);
188
+ }
189
+
190
+ async addSeenIds(ids: string[]): Promise<SeenState> {
191
+ const state = await this.loadSeenState();
192
+ const set = new Set(state.itemIds);
193
+ for (const id of ids) set.add(String(id));
194
+ const next: SeenState = { ...state, itemIds: [...set].sort() };
195
+ await this.saveSeenState(next);
196
+ return next;
197
+ }
198
+
199
+ async loadLastRun(): Promise<LastRunMeta | null> {
200
+ const kv = await loadKv();
201
+ return kv.get<LastRunMeta>(LAST_RUN_KV_KEY);
202
+ }
203
+
204
+ async saveLastRun(meta: LastRunMeta): Promise<void> {
205
+ const kv = await loadKv();
206
+ await kv.set(LAST_RUN_KV_KEY, meta);
207
+ }
208
+ }
209
+
210
+ const storeCache = new Map<string, ProofStore>();
211
+
212
+ export function resolveProofStore(agentRoot: string): ProofStore {
213
+ const key = `${resolveProofStoreBackend()}:${agentRoot}`;
214
+ const cached = storeCache.get(key);
215
+ if (cached) return cached;
216
+ const store =
217
+ resolveProofStoreBackend() === "vercel"
218
+ ? new VercelProofStore()
219
+ : new FsProofStore(agentRoot);
220
+ storeCache.set(key, store);
221
+ return store;
222
+ }
223
+
224
+ export async function readProofMarkdown(agentRoot: string): Promise<string> {
225
+ return resolveProofStore(agentRoot).readProofMarkdown();
226
+ }
227
+
228
+ export async function appendProofRun(
229
+ agentRoot: string,
230
+ section: { runAt: string; body: string },
231
+ ): Promise<void> {
232
+ return resolveProofStore(agentRoot).appendProofRun(section);
233
+ }
234
+
235
+ export async function loadSeenState(agentRoot: string): Promise<SeenState> {
236
+ return resolveProofStore(agentRoot).loadSeenState();
237
+ }
238
+
239
+ export async function saveSeenState(agentRoot: string, state: SeenState): Promise<void> {
240
+ return resolveProofStore(agentRoot).saveSeenState(state);
241
+ }
242
+
243
+ export async function addSeenIds(agentRoot: string, ids: string[]): Promise<SeenState> {
244
+ return resolveProofStore(agentRoot).addSeenIds(ids);
245
+ }
246
+
247
+ export async function saveLastRun(agentRoot: string, meta: LastRunMeta): Promise<void> {
248
+ return resolveProofStore(agentRoot).saveLastRun(meta);
249
+ }
250
+
251
+ export async function loadLastRun(agentRoot: string): Promise<LastRunMeta | null> {
252
+ return resolveProofStore(agentRoot).loadLastRun();
253
+ }
254
+
255
+ export function tavilyConfigured(): boolean {
256
+ return Boolean(process.env.TAVILY_API_TOKEN?.trim());
257
+ }
258
+
259
+ export function gatewayConfigured(): boolean {
260
+ return Boolean(
261
+ process.env.AI_GATEWAY_API_KEY?.trim() ||
262
+ process.env.AI_API_GATEWAY_KEY?.trim() ||
263
+ process.env.AI_GATEWAY_KEY?.trim(),
264
+ );
265
+ }
@@ -0,0 +1,214 @@
1
+ import {
2
+ type AuthoringContext,
3
+ } from "@plasm_lang/vercel-agent";
4
+
5
+ import {
6
+ addSeenIds,
7
+ appendProofRun,
8
+ gatewayConfigured,
9
+ loadLastRun,
10
+ loadSeenState,
11
+ saveLastRun,
12
+ saveSeenState,
13
+ tavilyConfigured,
14
+ } from "./proof-store.js";
15
+
16
+ export const MCP_RADAR_INTENT =
17
+ "track MCP innovations from Hacker News and corroborate with Tavily web search";
18
+
19
+ export const MCP_SEARCH_QUERY = "MCP OR Model Context Protocol";
20
+
21
+ export interface HnStoryCandidate {
22
+ id: string;
23
+ title?: string;
24
+ url?: string;
25
+ }
26
+
27
+ export interface RadarRunOptions {
28
+ force?: boolean;
29
+ }
30
+
31
+ export interface RadarRunResult {
32
+ ok: boolean;
33
+ skipped: boolean;
34
+ reason?: string;
35
+ candidates: HnStoryCandidate[];
36
+ newCandidates: HnStoryCandidate[];
37
+ agentText?: string;
38
+ error?: string;
39
+ }
40
+
41
+ type HnStubModule = {
42
+ item_search?: (
43
+ input: { query: string; tags?: string; per_page?: number },
44
+ options?: { transport?: unknown },
45
+ ) => Promise<Array<{ id?: string | number; title?: string; url?: string }>>;
46
+ };
47
+
48
+ export async function preflightHnMcpStories(ctx: AuthoringContext): Promise<HnStoryCandidate[]> {
49
+ try {
50
+ const mod = (await ctx.importStub("hackernews")) as HnStubModule;
51
+ if (typeof mod.item_search !== "function") return [];
52
+ const rows = await mod.item_search(
53
+ { query: MCP_SEARCH_QUERY, tags: "story", per_page: 10 },
54
+ );
55
+ return rows
56
+ .map((row) => ({
57
+ id: String(row.id ?? ""),
58
+ title: row.title,
59
+ url: row.url,
60
+ }))
61
+ .filter((row) => row.id.length > 0);
62
+ } catch (err) {
63
+ console.warn("[mcp-radar] preflight HN search failed:", err);
64
+ return [];
65
+ }
66
+ }
67
+
68
+ function filterNewCandidates(
69
+ candidates: HnStoryCandidate[],
70
+ seenIds: Set<string>,
71
+ force: boolean,
72
+ ): HnStoryCandidate[] {
73
+ if (force) return candidates;
74
+ return candidates.filter((c) => !seenIds.has(c.id));
75
+ }
76
+
77
+ function buildAgentGoal(newCandidates: HnStoryCandidate[], tavily: boolean): string {
78
+ const lines = newCandidates.map(
79
+ (c) => `- id=${c.id} title=${JSON.stringify(c.title ?? "")} url=${JSON.stringify(c.url ?? "")}`,
80
+ );
81
+ const tavilyNote = tavily
82
+ ? "Tavily is configured — corroborate each story with web_search."
83
+ : "Tavily is NOT configured — HN-only synthesis; use Confidence: low.";
84
+ return [
85
+ "Produce MCP proof entries for these NEW Hacker News story candidates:",
86
+ ...lines,
87
+ "",
88
+ tavilyNote,
89
+ "",
90
+ "Use plasm_context with stable intent for hackernews + tavily, then plan and run programs.",
91
+ "Output markdown proof blocks per the mcp-proof-format skill (### headers).",
92
+ "Do not skip candidates unless they are clearly unrelated to MCP.",
93
+ ].join("\n");
94
+ }
95
+
96
+ export async function runRadar(
97
+ ctx: AuthoringContext,
98
+ options: RadarRunOptions = {},
99
+ ): Promise<RadarRunResult> {
100
+ const force = options.force === true;
101
+ const seen = await loadSeenState(ctx.agentRoot);
102
+ const seenSet = new Set(seen.itemIds);
103
+
104
+ const candidates = await preflightHnMcpStories(ctx);
105
+ const newCandidates = filterNewCandidates(candidates, seenSet, force);
106
+
107
+ if (!newCandidates.length && !force) {
108
+ const result: RadarRunResult = {
109
+ ok: true,
110
+ skipped: true,
111
+ reason: candidates.length ? "no_new_stories" : "no_hn_hits",
112
+ candidates,
113
+ newCandidates: [],
114
+ };
115
+ await saveSeenState(ctx.agentRoot, {
116
+ ...seen,
117
+ lastRunAt: new Date().toISOString(),
118
+ lastRunStatus: "skipped",
119
+ lastNewCount: 0,
120
+ });
121
+ await saveLastRun(ctx.agentRoot, {
122
+ at: new Date().toISOString(),
123
+ status: "skipped",
124
+ newItems: 0,
125
+ message: result.reason,
126
+ });
127
+ return result;
128
+ }
129
+
130
+ if (!gatewayConfigured()) {
131
+ const result: RadarRunResult = {
132
+ ok: false,
133
+ skipped: true,
134
+ reason: "ai_gateway_missing",
135
+ candidates,
136
+ newCandidates,
137
+ error: "AI_GATEWAY_API_KEY is required for agent synthesis",
138
+ };
139
+ await saveLastRun(ctx.agentRoot, {
140
+ at: new Date().toISOString(),
141
+ status: "error",
142
+ newItems: 0,
143
+ message: result.error,
144
+ });
145
+ return result;
146
+ }
147
+
148
+ const toProcess = newCandidates.length ? newCandidates : candidates;
149
+ const goal = buildAgentGoal(toProcess, tavilyConfigured());
150
+
151
+ try {
152
+ const agent = await ctx.getAgent();
153
+ const turn = await agent.generate(goal, { resetConversation: false });
154
+ const runAt = new Date().toISOString();
155
+
156
+ if (turn.text.trim()) {
157
+ await appendProofRun(ctx.agentRoot, { runAt, body: turn.text });
158
+ }
159
+
160
+ const ids = toProcess.map((c) => c.id);
161
+ await addSeenIds(ctx.agentRoot, ids);
162
+ await saveSeenState(ctx.agentRoot, {
163
+ ...(await loadSeenState(ctx.agentRoot)),
164
+ lastRunAt: runAt,
165
+ lastRunStatus: "ok",
166
+ lastNewCount: toProcess.length,
167
+ });
168
+ await saveLastRun(ctx.agentRoot, {
169
+ at: runAt,
170
+ status: "ok",
171
+ newItems: toProcess.length,
172
+ });
173
+
174
+ return {
175
+ ok: true,
176
+ skipped: false,
177
+ candidates,
178
+ newCandidates: toProcess,
179
+ agentText: turn.text,
180
+ };
181
+ } catch (err) {
182
+ const message = String(err);
183
+ await saveSeenState(ctx.agentRoot, {
184
+ ...(await loadSeenState(ctx.agentRoot)),
185
+ lastRunAt: new Date().toISOString(),
186
+ lastRunStatus: "error",
187
+ });
188
+ await saveLastRun(ctx.agentRoot, {
189
+ at: new Date().toISOString(),
190
+ status: "error",
191
+ newItems: 0,
192
+ message,
193
+ });
194
+ return {
195
+ ok: false,
196
+ skipped: false,
197
+ candidates,
198
+ newCandidates: toProcess,
199
+ error: message,
200
+ };
201
+ }
202
+ }
203
+
204
+ export async function radarStatus(agentRoot: string): Promise<Record<string, unknown>> {
205
+ const seen = await loadSeenState(agentRoot);
206
+ const last = await loadLastRun(agentRoot);
207
+ return {
208
+ gateway: gatewayConfigured(),
209
+ tavily: tavilyConfigured(),
210
+ seenCount: seen.itemIds.length,
211
+ lastRun: last,
212
+ intent: MCP_RADAR_INTENT,
213
+ };
214
+ }
@@ -0,0 +1,17 @@
1
+ import { defineNitroConfig } from "nitropack/config";
2
+
3
+ export default defineNitroConfig({
4
+ compatibilityDate: "2026-06-26",
5
+ srcDir: ".",
6
+ ignore: ["api/**"],
7
+ devServer: {
8
+ port: Number(process.env.PORT ?? 3000),
9
+ host: process.env.HOST ?? "127.0.0.1",
10
+ },
11
+ typescript: {
12
+ strict: false,
13
+ },
14
+ externals: {
15
+ inline: ["@plasm_lang/engine"],
16
+ },
17
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "mcp-radar-agent",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Event-driven MCP innovation radar — Hacker News + Tavily proof log",
6
+ "scripts": {
7
+ "smoke:channel": "tsx scripts/smoke-channel.ts",
8
+ "eval": "tsx scripts/run-evals.ts"
9
+ },
10
+ "dependencies": {},
11
+ "devDependencies": {
12
+ "tsx": "^4.19.4"
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>MCP Radar Agent</title>
6
+ </head>
7
+ <body>
8
+ <p>MCP Radar Agent — use <code>/channel/mcp-radar/status</code> or <code>/plasm/v1/info</code>.</p>
9
+ </body>
10
+ </html>
@@ -0,0 +1,29 @@
1
+ import path from "node:path";
2
+
3
+ import { fromNodeMiddleware } from "h3";
4
+
5
+ import agentDefinition from "../agent/agent.js";
6
+ import { createPlasmApp, vercelPlasmHandler } from "@plasm_lang/vercel-agent";
7
+
8
+ const agentRoot = path.join(process.cwd(), "agent");
9
+
10
+ let appPromise: ReturnType<typeof createPlasmApp> | undefined;
11
+
12
+ async function plasmApp() {
13
+ appPromise ??= createPlasmApp({
14
+ agentRoot,
15
+ definition: agentDefinition,
16
+ mode: "prod",
17
+ sessions: false,
18
+ });
19
+ return appPromise;
20
+ }
21
+
22
+ export default fromNodeMiddleware(async (req, res) => {
23
+ const app = await plasmApp();
24
+ await new Promise<void>((resolve, reject) => {
25
+ res.once("finish", () => resolve());
26
+ res.once("error", reject);
27
+ void vercelPlasmHandler(app)(req, res).catch(reject);
28
+ });
29
+ });