@plasm_lang/vercel-agent 0.3.78 → 0.3.80

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
package/src/cli/init.ts CHANGED
@@ -1,112 +1,17 @@
1
- import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
 
5
- import { FRAMEWORK_VERSION } from "../project-info.js";
4
+ import {
5
+ blankPackageJsonScaffold,
6
+ exists,
7
+ patchProjectPackageJson,
8
+ plasmAgentPackageRoot,
9
+ runTemplateInit,
10
+ writeDeployScaffold,
11
+ } from "./init-scaffold.js";
6
12
  import type { ResolvedAgentProject } from "./project-root.js";
7
13
 
8
- export interface InitOptions {
9
- template?: string;
10
- }
11
-
12
- const SKIP_TEMPLATE_DIRS = new Set(["node_modules", ".plasm", ".nitro", ".output", "server"]);
13
- const SKIP_TEMPLATE_FILES = new Set(["package-lock.json"]);
14
-
15
- function plasmAgentPackageRoot(): string {
16
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
17
- }
18
-
19
- function resolveTemplateDir(template: string): string {
20
- const templates: Record<string, string> = {
21
- "mcp-radar": path.join(plasmAgentPackageRoot(), "../../../examples/mcp-radar-agent"),
22
- };
23
- const dir = templates[template];
24
- if (!dir) {
25
- throw new Error(
26
- `Unknown template "${template}". Available: ${Object.keys(templates).join(", ")}`,
27
- );
28
- }
29
- return dir;
30
- }
31
-
32
- function shouldCopyTemplateEntry(src: string, templateRoot: string): boolean {
33
- const rel = path.relative(templateRoot, src);
34
- if (!rel || rel === "") return true;
35
- const parts = rel.split(path.sep);
36
- if (parts.some((part) => SKIP_TEMPLATE_DIRS.has(part))) return false;
37
- if (SKIP_TEMPLATE_FILES.has(path.basename(src))) return false;
38
- return true;
39
- }
40
-
41
- async function copyTemplate(templateRoot: string, projectRoot: string): Promise<void> {
42
- await cp(templateRoot, projectRoot, {
43
- recursive: true,
44
- filter: (src) => src === templateRoot || shouldCopyTemplateEntry(src, templateRoot),
45
- });
46
- }
47
-
48
- async function patchBootstrapPackageJson(
49
- projectRoot: string,
50
- packageRoot: string,
51
- ): Promise<void> {
52
- const pkgPath = path.join(projectRoot, "package.json");
53
- const raw = await readFile(pkgPath, "utf8");
54
- const pkg = JSON.parse(raw) as {
55
- name?: string;
56
- scripts?: Record<string, string>;
57
- dependencies?: Record<string, string>;
58
- devDependencies?: Record<string, string>;
59
- };
60
- pkg.name = path.basename(projectRoot);
61
- const engineRoot = path.resolve(packageRoot, "../plasm-engine");
62
- pkg.dependencies = {
63
- ...pkg.dependencies,
64
- "@plasm_lang/vercel-agent": `file:${path.resolve(packageRoot)}`,
65
- "@plasm_lang/engine": `file:${engineRoot}`,
66
- "@vercel/blob": "^0.27.3",
67
- "@vercel/functions": "^3.4.3",
68
- "@vercel/kv": "^3.0.0",
69
- };
70
- const nodeRunner =
71
- "node --experimental-strip-types --experimental-transform-types ./node_modules/@plasm_lang/vercel-agent/scripts/plasm-node.mjs";
72
- pkg.scripts = {
73
- build: "plasm-agent build",
74
- "vercel-build": "plasm-agent build",
75
- dev: "plasm-agent dev",
76
- "dev:interactive": "plasm-agent dev --interactive",
77
- info: "plasm-agent info",
78
- deploy: "vercel deploy",
79
- ...pkg.scripts,
80
- eval: `${nodeRunner} scripts/run-evals.ts`,
81
- "smoke:channel": `${nodeRunner} scripts/smoke-channel.ts`,
82
- };
83
- pkg.devDependencies = {
84
- ...pkg.devDependencies,
85
- nitropack: "^2.13.4",
86
- };
87
- await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
88
- }
89
-
90
- async function runTemplateInit(
91
- targetDir: string,
92
- template: string,
93
- ): Promise<ResolvedAgentProject> {
94
- const projectRoot = path.resolve(targetDir);
95
- const agentRoot = path.join(projectRoot, "agent");
96
- if (await exists(path.join(agentRoot, "agent.ts"))) {
97
- throw new Error(`agent/agent.ts already exists in ${projectRoot}`);
98
- }
99
- const templateRoot = resolveTemplateDir(template);
100
- if (!(await exists(templateRoot))) {
101
- throw new Error(`Template source missing: ${templateRoot}`);
102
- }
103
- await mkdir(projectRoot, { recursive: true });
104
- await copyTemplate(templateRoot, projectRoot);
105
- await patchBootstrapPackageJson(projectRoot, plasmAgentPackageRoot());
106
- await writeVercelScaffold(projectRoot, template);
107
- await writeNitroScaffold(projectRoot);
108
- return { projectRoot, agentRoot };
109
- }
14
+ export type { InitOptions } from "./init-scaffold.js";
110
15
 
111
16
  const AGENT_TS = `import path from "node:path";
112
17
  import { fileURLToPath } from "node:url";
@@ -203,177 +108,12 @@ PLASM_TENANT_SCOPE=local
203
108
  PORT=3000
204
109
  `;
205
110
 
206
- const VERCEL_JSON_DEFAULT = `{
207
- "buildCommand": "plasm-agent build",
208
- "rewrites": [{ "source": "/(.*)", "destination": "/api/$1" }]
209
- }
210
- `;
211
-
212
- const VERCEL_JSON_MCP_RADAR = `{
213
- "buildCommand": "plasm-agent build",
214
- "rewrites": [{ "source": "/(.*)", "destination": "/api/$1" }],
215
- "crons": [
216
- {
217
- "path": "/internal/cron/mcp-radar-scan",
218
- "schedule": "0 */6 * * *"
219
- }
220
- ],
221
- "functions": {
222
- "api/**": {
223
- "maxDuration": 300
224
- }
225
- }
226
- }
227
- `;
228
-
229
- const VERCELIGNORE = `node_modules
230
- .env*
231
- agent/.plasm/research
232
- `;
233
-
234
- const API_HANDLER_TS = `import path from "node:path";
235
- import { fileURLToPath } from "node:url";
236
-
237
- import agentDefinition from "../agent/agent.js";
238
- import { createPlasmApp, vercelPlasmHandler } from "@plasm_lang/vercel-agent";
239
-
240
- const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
241
- const agentRoot = path.join(packageRoot, "agent");
242
-
243
- let app: Awaited<ReturnType<typeof createPlasmApp>> | undefined;
244
-
245
- export default async function handler(
246
- req: import("node:http").IncomingMessage,
247
- res: import("node:http").ServerResponse,
248
- ): Promise<void> {
249
- app ??= await createPlasmApp({
250
- agentRoot,
251
- definition: agentDefinition,
252
- mode: "prod",
253
- sessions: false,
254
- });
255
- await vercelPlasmHandler(app)(req, res);
256
- }
257
- `;
258
-
259
- const NITRO_CONFIG_TS = `import { defineNitroConfig } from "nitropack/config";
260
-
261
- export default defineNitroConfig({
262
- compatibilityDate: "2026-06-26",
263
- srcDir: ".",
264
- ignore: ["api/**"],
265
- devServer: {
266
- port: Number(process.env.PORT ?? 3000),
267
- host: process.env.HOST ?? "127.0.0.1",
268
- },
269
- typescript: {
270
- strict: false,
271
- },
272
- externals: {
273
- inline: ["@plasm_lang/engine"],
274
- },
275
- });
276
- `;
277
-
278
- const NITRO_CATCHALL_ROUTE_TS = `import path from "node:path";
279
-
280
- import { fromNodeMiddleware } from "h3";
281
-
282
- import agentDefinition from "../agent/agent.js";
283
- import { createPlasmApp, vercelPlasmHandler } from "@plasm_lang/vercel-agent";
284
-
285
- const agentRoot = path.join(process.cwd(), "agent");
286
-
287
- let appPromise: ReturnType<typeof createPlasmApp> | undefined;
288
-
289
- async function plasmApp() {
290
- appPromise ??= createPlasmApp({
291
- agentRoot,
292
- definition: agentDefinition,
293
- mode: "prod",
294
- sessions: false,
295
- });
296
- return appPromise;
297
- }
298
-
299
- export default fromNodeMiddleware(async (req, res) => {
300
- const app = await plasmApp();
301
- await new Promise<void>((resolve, reject) => {
302
- res.once("finish", () => resolve());
303
- res.once("error", reject);
304
- void vercelPlasmHandler(app)(req, res).catch(reject);
305
- });
306
- });
307
- `;
308
-
309
- const PACKAGE_JSON_TEMPLATE = {
310
- name: "my-plasm-agent",
311
- private: true,
312
- type: "module",
313
- scripts: {
314
- dev: "plasm-agent dev",
315
- "dev:interactive": "plasm-agent dev --interactive",
316
- build: "plasm-agent build",
317
- "vercel-build": "plasm-agent build",
318
- info: "plasm-agent info",
319
- deploy: "vercel deploy",
320
- },
321
- dependencies: {} as Record<string, string>,
322
- devDependencies: {
323
- nitropack: "^2.13.4",
324
- },
325
- };
326
-
327
- async function writeVercelScaffold(
328
- projectRoot: string,
329
- template?: string,
330
- ): Promise<void> {
331
- const vercelJson =
332
- template === "mcp-radar" ? VERCEL_JSON_MCP_RADAR : VERCEL_JSON_DEFAULT;
333
- await mkdir(path.join(projectRoot, "api"), { recursive: true });
334
- await writeFile(path.join(projectRoot, "vercel.json"), vercelJson, "utf8");
335
- await writeFile(path.join(projectRoot, ".vercelignore"), VERCELIGNORE, "utf8");
336
- await writeFile(
337
- path.join(projectRoot, "api", "[[...path]].ts"),
338
- API_HANDLER_TS,
339
- "utf8",
340
- );
341
- }
342
-
343
- async function writeNitroScaffold(projectRoot: string): Promise<void> {
344
- await mkdir(path.join(projectRoot, "routes"), { recursive: true });
345
- await writeFile(path.join(projectRoot, "nitro.config.ts"), NITRO_CONFIG_TS, "utf8");
346
- await writeFile(
347
- path.join(projectRoot, "routes", "[...path].ts"),
348
- NITRO_CATCHALL_ROUTE_TS,
349
- "utf8",
350
- );
351
- }
352
-
353
- function packageJsonScaffold(): Record<string, unknown> {
354
- return {
355
- ...PACKAGE_JSON_TEMPLATE,
356
- dependencies: {
357
- "@plasm_lang/vercel-agent": `^${FRAMEWORK_VERSION}`,
358
- },
359
- };
360
- }
361
-
362
- async function exists(p: string): Promise<boolean> {
363
- try {
364
- await access(p);
365
- return true;
366
- } catch {
367
- return false;
368
- }
369
- }
370
-
371
111
  export async function runPlasmInit(
372
112
  targetDir: string,
373
- options?: InitOptions,
113
+ options?: import("./init-scaffold.js").InitOptions,
374
114
  ): Promise<ResolvedAgentProject> {
375
115
  if (options?.template) {
376
- return runTemplateInit(targetDir, options.template);
116
+ return runTemplateInit(targetDir, options.template, options);
377
117
  }
378
118
 
379
119
  const projectRoot = path.resolve(targetDir);
@@ -399,21 +139,19 @@ export async function runPlasmInit(
399
139
  if (!(await exists(path.join(projectRoot, "package.json")))) {
400
140
  await writeFile(
401
141
  path.join(projectRoot, "package.json"),
402
- `${JSON.stringify(packageJsonScaffold(), null, 2)}\n`,
142
+ `${JSON.stringify(blankPackageJsonScaffold(), null, 2)}\n`,
403
143
  "utf8",
404
144
  );
405
- await patchBootstrapPackageJson(projectRoot, plasmAgentPackageRoot());
145
+ await patchProjectPackageJson(projectRoot, plasmAgentPackageRoot(), { npm: options?.npm });
406
146
  }
407
147
 
408
- await writeVercelScaffold(projectRoot);
409
- await writeNitroScaffold(projectRoot);
410
-
148
+ await writeDeployScaffold(projectRoot);
411
149
  return { projectRoot, agentRoot };
412
150
  }
413
151
 
414
152
  export function formatInitSuccess(
415
153
  project: ResolvedAgentProject,
416
- options?: InitOptions,
154
+ options?: import("./init-scaffold.js").InitOptions,
417
155
  ): string {
418
156
  if (options?.template === "mcp-radar") {
419
157
  return [
@@ -3,6 +3,7 @@ import { access } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
+ import { nitroDevNodeOptions } from "./node-dev-imports.js";
6
7
  import type { ResolvedAgentProject } from "./project-root.js";
7
8
 
8
9
  function plasmAgentPackageRoot(): string {
@@ -32,12 +33,7 @@ function nitroBin(projectRoot: string): string {
32
33
  export async function startNitroDevForProject(project: ResolvedAgentProject): Promise<void> {
33
34
  await assertNitroScaffold(project.projectRoot);
34
35
 
35
- const loader = path.join(plasmAgentPackageRoot(), "scripts", "register-plasm-loader.mjs");
36
- const nodeOptions = [
37
- "--experimental-strip-types",
38
- "--experimental-transform-types",
39
- `--import=${loader}`,
40
- ].join(" ");
36
+ const nodeOptions = nitroDevNodeOptions(plasmAgentPackageRoot());
41
37
 
42
38
  const bin = nitroBin(project.projectRoot);
43
39
  try {
@@ -0,0 +1,14 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ /** NODE_OPTIONS imports for Nitro dev (tsx + Plasm .ts resolution). */
8
+ export function nitroDevNodeOptions(packageRoot: string): string {
9
+ const tsxImport = require.resolve("tsx/esm");
10
+ const plasmLoader = pathToFileURL(
11
+ path.join(packageRoot, "scripts", "register-plasm-loader.mjs"),
12
+ ).href;
13
+ return [`--import=${tsxImport}`, `--import=${plasmLoader}`].join(" ");
14
+ }
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ let cachedVersion: string | undefined;
6
+
7
+ /** Published semver from @plasm_lang/vercel-agent package.json. */
8
+ export function frameworkPackageVersion(): string {
9
+ if (cachedVersion) return cachedVersion;
10
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ const raw = readFileSync(path.join(packageRoot, "package.json"), "utf8");
12
+ cachedVersion = (JSON.parse(raw) as { version: string }).version;
13
+ return cachedVersion;
14
+ }
@@ -10,6 +10,7 @@ import { isNativeEngineAvailable } from "./engine/napi-binding.js";
10
10
  import { exportScheduleCronManifest } from "./authoring/schedule-manager.js";
11
11
  import { isGatewayConfigured } from "./gateway-model.js";
12
12
  import type { LoadedProjectSlots } from "./authoring/slot-loader.js";
13
+ import { frameworkPackageVersion } from "./package-version.js";
13
14
  import { resolveCatalogLiveHash } from "./stubs/catalog-hash.js";
14
15
  import { stubFreshness } from "./stubs/generator.js";
15
16
 
@@ -21,7 +22,7 @@ export const PLASM_LANGUAGE_TOOLS = [
21
22
  ] as const;
22
23
 
23
24
  export const FRAMEWORK_NAME = "@plasm_lang/vercel-agent";
24
- export const FRAMEWORK_VERSION = "0.0.1";
25
+ export const FRAMEWORK_VERSION = frameworkPackageVersion();
25
26
 
26
27
  export interface CatalogInfoEntry {
27
28
  name: string;
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ .env*
3
+ agent/.plasm/research
@@ -0,0 +1,139 @@
1
+ # MCP Radar Agent
2
+
3
+ Event-driven research example for [`@plasm_lang/vercel-agent`](../../plasm-oss/packages/plasm-agent/): watch Hacker News for **MCP** innovation signals, corroborate with **Tavily**, and append structured entries to a maintained proof log.
4
+
5
+ ## Purpose
6
+
7
+ Demonstrates Plasm’s **dual surface**:
8
+
9
+ - **Code surface** — channel/schedule handlers preflight HN via typed stubs and own proof persistence
10
+ - **Model surface** — federated `plasm_context` (hackernews + tavily) → `plasm` → `plasm_run` for synthesis
11
+
12
+ ## Bootstrap from CLI
13
+
14
+ Scaffold a fresh copy (verified by `npm run smoke:bootstrap` in the framework package):
15
+
16
+ ```bash
17
+ mkdir /tmp/my-radar && cd /tmp/my-radar
18
+ plasm-agent init --template mcp-radar .
19
+ npm install
20
+ plasm-agent build
21
+ npm run smoke:channel
22
+ plasm-agent dev
23
+ ```
24
+
25
+ The canonical template source is this directory (`examples/mcp-radar-agent/`).
26
+
27
+ ## Deploy to Vercel
28
+
29
+ Low-friction production path (eve-aligned): `vercel.json` + catch-all `api/[[...path]].ts` mounts the same routes as local dev (channels, cron, `/plasm/v1/info`).
30
+
31
+ ```bash
32
+ cd examples/mcp-radar-agent # or your bootstrapped project
33
+ plasm-agent link
34
+ plasm-agent build
35
+ vercel env pull .env.local # AI_GATEWAY_API_KEY, CRON_SECRET, KV, Blob
36
+ vercel deploy
37
+ curl -s "$DEPLOY_URL/channel/mcp-radar/status" | jq .
38
+ ```
39
+
40
+ | Variable | Deploy |
41
+ |----------|--------|
42
+ | `AI_GATEWAY_API_KEY` | Required for agent synthesis |
43
+ | `CRON_SECRET` | Required — Vercel Cron hits `/internal/cron/mcp-radar-scan` |
44
+ | `KV_REST_API_URL` + `KV_REST_API_TOKEN` | Durable seen-items + last-run state |
45
+ | `BLOB_READ_WRITE_TOKEN` | Durable proof markdown log |
46
+ | `TAVILY_API_TOKEN` | Optional corroboration |
47
+
48
+ On Vercel, `POST /channel/mcp-radar/run` returns `202` and continues in the background via `waitUntil`. Locally it runs synchronously for smoke tests.
49
+
50
+ Monorepo deploy: set Vercel **Root Directory** to `examples/mcp-radar-agent`.
51
+
52
+ ## Setup (monorepo checkout)
53
+
54
+ ```bash
55
+ cd examples/mcp-radar-agent
56
+ npm install
57
+ cp .env.example .env.local
58
+ ```
59
+
60
+ | Variable | Required | Role |
61
+ |----------|----------|------|
62
+ | `AI_GATEWAY_API_KEY` | Yes (live runs) | Agent model turns |
63
+ | `TAVILY_API_TOKEN` | Optional | Tavily corroboration; HN-only when unset |
64
+
65
+ Build CGS stubs (symlinked catalogs):
66
+
67
+ ```bash
68
+ npm run build
69
+ ```
70
+
71
+ ## Dev
72
+
73
+ ```bash
74
+ npm run build
75
+ npm run dev # Nitro dev server — Vercel routing parity (channels, cron, /plasm/v1/*)
76
+ npm run dev:interactive # optional: in-process server + TUI + sessions + hot reload
77
+ ```
78
+
79
+ `plasm-agent dev` starts Nitro by default so channel routes like `/channel/mcp-radar/status` match production.
80
+
81
+ Slash commands in the interactive TUI: `/info`, `/catalogs`, `/new`, `/quit`.
82
+
83
+ ## Channel API
84
+
85
+ Start headless dev server (`npm run dev:headless`), then:
86
+
87
+ ```bash
88
+ BASE=http://127.0.0.1:3000
89
+
90
+ # Status
91
+ curl -s "$BASE/channel/mcp-radar/status" | jq .
92
+
93
+ # Read proof log (markdown)
94
+ curl -s "$BASE/channel/mcp-radar/proof"
95
+
96
+ # Trigger one scan (dedupes seen HN ids)
97
+ curl -s -X POST "$BASE/channel/mcp-radar/run" -H 'content-type: application/json' -d '{}'
98
+
99
+ # Force scan even when no new ids
100
+ curl -s -X POST "$BASE/channel/mcp-radar/run" -H 'content-type: application/json' -d '{"force":true}'
101
+ ```
102
+
103
+ Proof artifact (local fs): [`agent/research/mcp-innovations-proof.md`](./agent/research/mcp-innovations-proof.md)
104
+
105
+ Dedupe state (local fs): `agent/.plasm/research/seen-hn-items.json` (gitignored). On Vercel deploy, proof + dedupe use Blob + KV when env vars are set.
106
+
107
+ ## Schedule
108
+
109
+ `agent/schedules/mcp-radar-scan.ts` runs every **6 hours** (`0 */6 * * *`). Vercel Cron calls `/internal/cron/mcp-radar-scan` with `Authorization: Bearer $CRON_SECRET`.
110
+
111
+ ## Evals
112
+
113
+ ```bash
114
+ npm run eval # requires AI_GATEWAY_API_KEY
115
+ ```
116
+
117
+ Live eval: `evals/mcp-radar-discover.eval.ts`
118
+
119
+ ## Smoke
120
+
121
+ ```bash
122
+ npm run build
123
+ npm run smoke:channel
124
+ ```
125
+
126
+ ## Catalogs
127
+
128
+ Symlinked from monorepo `apis/`:
129
+
130
+ - `agent/catalogs/hackernews` → `plasm-oss/apis/hackernews`
131
+ - `agent/catalogs/tavily` → `plasm-oss/apis/tavily`
132
+
133
+ ## Stable session intent
134
+
135
+ ```
136
+ track MCP innovations from Hacker News and corroborate with Tavily web search
137
+ ```
138
+
139
+ Federated seeds: `hackernews:Item`, `tavily:SearchResult`
@@ -0,0 +1,38 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import {
5
+ createAgentFromProject,
6
+ createProductionHostTransport,
7
+ defineAgent,
8
+ loadAgentEnv,
9
+ } from "@plasm_lang/vercel-agent";
10
+
11
+ loadAgentEnv();
12
+
13
+ const agentRoot = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ const agentDefinition = defineAgent({
16
+ model: process.env.PLASM_AGENT_MODEL ?? "anthropic/claude-sonnet-4.6",
17
+ compaction: { thresholdPercent: 0.75 },
18
+ modelOptions: { temperature: 0.2 },
19
+ experimental: {
20
+ workflow: { world: { type: "vercel" } },
21
+ skills: true,
22
+ },
23
+ build: {
24
+ externalDependencies: ["@plasm_lang/engine"],
25
+ },
26
+ });
27
+
28
+ export default agentDefinition;
29
+
30
+ export async function createPlasmAgent() {
31
+ return createAgentFromProject(agentDefinition, {
32
+ agentRoot,
33
+ tenantScope: process.env.PLASM_TENANT_SCOPE ?? "mcp-radar",
34
+ maxSteps: 24,
35
+ telemetry: process.env.PLASM_AGENT_TELEMETRY !== "0",
36
+ hostTransport: createProductionHostTransport(),
37
+ });
38
+ }
@@ -0,0 +1,23 @@
1
+ # Hacker News (Firebase API)
2
+
3
+ Read-only mirror of the public [Hacker News API](https://github.com/HackerNews/API) plus the [Algolia HN search API](https://hn.algolia.com/api) (full-URL CML; same `http_backend` is Firebase for `item_get`). No authentication.
4
+
5
+ ## Scope
6
+
7
+ - **item_search** / **item_search_by_date** — Algolia `search` and `search_by_date` (default `tags=story`). Rows use `objectID` as `id`; use **item_get** to load the Firebase item.
8
+ - **item_feed_query** — ordered item ids for `top`, `new`, `best`, `ask`, `show`, or `job` (ids only; use **item_get** to hydrate).
9
+ - **max_item_id_query** — current largest item id (`maxitem.json` returns a bare integer; mapped via `wrap_root_scalar`).
10
+ - **recent_updated_item_query** / **recent_updated_user_query** — live slices from `updates.json` (`items` and `profiles` arrays; profiles are usernames only).
11
+ - **item_get** / **user_get** — full JSON for an item or user. Poll stories expose **`parts`** (option ids) and the **`poll_options`** relation for chaining.
12
+
13
+ ## Try it
14
+
15
+ ```bash
16
+ cargo run --bin plasm-repl -- --schema apis/hackernews
17
+ ```
18
+
19
+ Eval coverage (no LLM):
20
+
21
+ ```bash
22
+ cargo run -p plasm-eval -- coverage --schema apis/hackernews --cases apis/hackernews/eval/cases.yaml
23
+ ```