@plasm_lang/vercel-agent 0.3.86 → 0.3.87

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 (38) hide show
  1. package/README.md +8 -10
  2. package/package.json +2 -2
  3. package/src/archive/adapters.ts +4 -0
  4. package/src/archive/prod-archive-store.ts +28 -45
  5. package/src/archive/resolve-backend.ts +8 -26
  6. package/src/archive/types.ts +1 -0
  7. package/src/archive/vercel-blob-adapter.ts +5 -0
  8. package/src/authoring/schedule-manager.ts +36 -22
  9. package/src/cli/build-manifest.ts +18 -0
  10. package/src/cli/build.ts +4 -13
  11. package/src/cli/compile-authored-slots.ts +9 -0
  12. package/src/cli/init-scaffold.ts +6 -2
  13. package/src/cli/init.ts +2 -6
  14. package/src/index.ts +3 -2
  15. package/src/nitro/agent-summary.ts +7 -7
  16. package/src/nitro/build-application.ts +43 -0
  17. package/src/nitro/create-plasm-nitro.ts +16 -2
  18. package/src/nitro/ensure-workflow-builder-dirs.ts +42 -0
  19. package/src/nitro/patch-vercel-config.ts +0 -3
  20. package/src/nitro/prepare-host.ts +5 -6
  21. package/src/nitro/workflow-directives.ts +1 -0
  22. package/src/nitro/write-nitro-schedule-tasks.ts +67 -0
  23. package/src/nitro/write-workflow-dispatch-route.ts +68 -0
  24. package/src/operator/routes.ts +6 -6
  25. package/src/project-info.ts +10 -6
  26. package/src/server/plasm-handler.ts +4 -4
  27. package/src/state/blob-state-adapter.ts +65 -0
  28. package/src/state/define-state.ts +10 -2
  29. package/src/state/fs-state-adapter.ts +1 -1
  30. package/src/storage/vercel-blob.ts +47 -0
  31. package/templates/mcp-radar/README.md +19 -16
  32. package/templates/mcp-radar/agent/channels/mcp-radar.ts +7 -7
  33. package/templates/mcp-radar/agent/schedules/mcp-radar-scan.ts +3 -5
  34. package/templates/mcp-radar/lib/proof-store.ts +43 -44
  35. package/templates/mcp-radar/lib/run-radar.ts +1 -1
  36. package/templates/mcp-radar/lib/start-mcp-radar.ts +12 -0
  37. package/templates/mcp-radar/scripts/provision-vercel-storage.mjs +18 -0
  38. package/templates/mcp-radar/workflows/mcp-radar-scan.ts +26 -0
@@ -4,7 +4,8 @@ import { waitUntil } from "@vercel/functions";
4
4
  import { defineChannel } from "@plasm_lang/vercel-agent";
5
5
 
6
6
  import { readProofMarkdown } from "../../lib/proof-store.js";
7
- import { radarStatus, runRadar } from "../../lib/run-radar.js";
7
+ import { radarStatus } from "../../lib/run-radar.js";
8
+ import { startMcpRadarRun } from "../../lib/start-mcp-radar.js";
8
9
 
9
10
  function readJsonBody(req: IncomingMessage): Promise<unknown> {
10
11
  return new Promise((resolve, reject) => {
@@ -45,17 +46,16 @@ export default defineChannel({
45
46
 
46
47
  if (process.env.VERCEL === "1") {
47
48
  waitUntil(
48
- runRadar(ctx, options).catch((err: unknown) => {
49
- console.error("[mcp-radar] background run failed:", err);
49
+ startMcpRadarRun(ctx, options).catch((err: unknown) => {
50
+ console.error("[mcp-radar] workflow start failed:", err);
50
51
  }),
51
52
  );
52
- sendJson(res, 202, { accepted: true, async: true });
53
+ sendJson(res, 202, { accepted: true, workflow: true });
53
54
  return;
54
55
  }
55
56
 
56
- const result = await runRadar(ctx, options);
57
- const status = result.ok ? 200 : 500;
58
- sendJson(res, status, result);
57
+ const run = await startMcpRadarRun(ctx, options);
58
+ sendJson(res, 200, { accepted: true, workflow: true, run });
59
59
  },
60
60
  },
61
61
  {
@@ -1,14 +1,12 @@
1
1
  import { defineSchedule } from "@plasm_lang/vercel-agent";
2
2
 
3
- import { runRadar } from "../../lib/run-radar.js";
3
+ import { startMcpRadarRun } from "../../lib/start-mcp-radar.js";
4
4
 
5
5
  export default defineSchedule({
6
6
  name: "mcp-radar-scan",
7
7
  cron: "0 */6 * * *",
8
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
- );
9
+ await startMcpRadarRun(ctx);
10
+ console.log("[mcp-radar:schedule] workflow started");
13
11
  },
14
12
  });
@@ -1,6 +1,20 @@
1
1
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ function isVercelHosted(): boolean {
5
+ return (
6
+ process.env.VERCEL === "1" ||
7
+ Boolean(process.env.VERCEL_DEPLOYMENT_ID?.trim()) ||
8
+ Boolean(process.env.VERCEL_ENV?.trim())
9
+ );
10
+ }
11
+
12
+ function hasLinkedBlobStore(): boolean {
13
+ return Boolean(
14
+ process.env.BLOB_READ_WRITE_TOKEN?.trim() || process.env.BLOB_STORE_ID?.trim(),
15
+ );
16
+ }
17
+
4
18
  export interface SeenState {
5
19
  itemIds: string[];
6
20
  lastRunAt?: string;
@@ -28,9 +42,9 @@ export interface ProofStore {
28
42
  saveLastRun(meta: LastRunMeta): Promise<void>;
29
43
  }
30
44
 
31
- const SEEN_KV_KEY = "plasm:mcp-radar:seen";
32
- const LAST_RUN_KV_KEY = "plasm:mcp-radar:last-run";
33
45
  const PROOF_BLOB_KEY = "research/mcp-innovations-proof.md";
46
+ const SEEN_BLOB_KEY = "research/seen-hn-items.json";
47
+ const LAST_RUN_BLOB_KEY = "research/last-run.json";
34
48
  const PROOF_HEADER = "# MCP Innovations Proof Log\n\n";
35
49
 
36
50
  function researchDir(agentRoot: string): string {
@@ -52,24 +66,10 @@ function lastRunPath(agentRoot: string): string {
52
66
  export function resolveProofStoreBackend(): ProofStoreBackend {
53
67
  const explicit = process.env.PLASM_PROOF_STORE_BACKEND?.trim().toLowerCase();
54
68
  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";
69
+ if (isVercelHosted() || hasLinkedBlobStore()) return "vercel";
60
70
  return "fs";
61
71
  }
62
72
 
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
73
  async function blobGetText(key: string): Promise<string | null> {
74
74
  const { head } = await import("@vercel/blob");
75
75
  const meta = await head(key).catch(() => null);
@@ -84,6 +84,26 @@ async function blobPutText(key: string, body: string): Promise<void> {
84
84
  await put(key, body, { access: "public", addRandomSuffix: false });
85
85
  }
86
86
 
87
+ async function blobGetJson<T>(key: string): Promise<T | null> {
88
+ const text = await blobGetText(key);
89
+ if (!text) return null;
90
+ return JSON.parse(text) as T;
91
+ }
92
+
93
+ async function blobPutJson(key: string, value: unknown): Promise<void> {
94
+ await blobPutText(key, `${JSON.stringify(value, null, 2)}\n`);
95
+ }
96
+
97
+ function parseSeenState(parsed: SeenState | null): SeenState {
98
+ if (!parsed) return { itemIds: [] };
99
+ return {
100
+ itemIds: Array.isArray(parsed.itemIds) ? parsed.itemIds.map(String) : [],
101
+ lastRunAt: parsed.lastRunAt,
102
+ lastRunStatus: parsed.lastRunStatus,
103
+ lastNewCount: parsed.lastNewCount,
104
+ };
105
+ }
106
+
87
107
  class FsProofStore implements ProofStore {
88
108
  constructor(private readonly agentRoot: string) {}
89
109
 
@@ -110,13 +130,7 @@ class FsProofStore implements ProofStore {
110
130
  async loadSeenState(): Promise<SeenState> {
111
131
  try {
112
132
  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
- };
133
+ return parseSeenState(JSON.parse(raw) as SeenState);
120
134
  } catch {
121
135
  return { itemIds: [] };
122
136
  }
@@ -171,20 +185,11 @@ class VercelProofStore implements ProofStore {
171
185
  }
172
186
 
173
187
  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
- };
188
+ return parseSeenState(await blobGetJson<SeenState>(SEEN_BLOB_KEY));
183
189
  }
184
190
 
185
191
  async saveSeenState(state: SeenState): Promise<void> {
186
- const kv = await loadKv();
187
- await kv.set(SEEN_KV_KEY, state);
192
+ await blobPutJson(SEEN_BLOB_KEY, state);
188
193
  }
189
194
 
190
195
  async addSeenIds(ids: string[]): Promise<SeenState> {
@@ -197,13 +202,11 @@ class VercelProofStore implements ProofStore {
197
202
  }
198
203
 
199
204
  async loadLastRun(): Promise<LastRunMeta | null> {
200
- const kv = await loadKv();
201
- return kv.get<LastRunMeta>(LAST_RUN_KV_KEY);
205
+ return blobGetJson<LastRunMeta>(LAST_RUN_BLOB_KEY);
202
206
  }
203
207
 
204
208
  async saveLastRun(meta: LastRunMeta): Promise<void> {
205
- const kv = await loadKv();
206
- await kv.set(LAST_RUN_KV_KEY, meta);
209
+ await blobPutJson(LAST_RUN_BLOB_KEY, meta);
207
210
  }
208
211
  }
209
212
 
@@ -264,9 +267,5 @@ export function gatewayConfigured(): boolean {
264
267
  ) {
265
268
  return true;
266
269
  }
267
- return (
268
- process.env.VERCEL === "1" ||
269
- Boolean(process.env.VERCEL_DEPLOYMENT_ID?.trim()) ||
270
- Boolean(process.env.VERCEL_ENV?.trim())
271
- );
270
+ return isVercelHosted();
272
271
  }
@@ -134,7 +134,7 @@ export async function runRadar(
134
134
  reason: "ai_gateway_missing",
135
135
  candidates,
136
136
  newCandidates,
137
- error: "AI_GATEWAY_API_KEY is required for agent synthesis",
137
+ error: "AI Gateway is not configured (link project on Vercel or set AI_GATEWAY_API_KEY locally)",
138
138
  };
139
139
  await saveLastRun(ctx.agentRoot, {
140
140
  at: new Date().toISOString(),
@@ -0,0 +1,12 @@
1
+ import type { AuthoringContext } from "@plasm_lang/vercel-agent";
2
+
3
+ import type { RadarRunOptions } from "./run-radar.js";
4
+
5
+ export async function startMcpRadarRun(
6
+ ctx: AuthoringContext,
7
+ options: RadarRunOptions = {},
8
+ ): Promise<unknown> {
9
+ const { start } = await import("workflow/api");
10
+ const { mcpRadarScanWorkflow } = await import("../workflows/mcp-radar-scan.js");
11
+ return start(mcpRadarScanWorkflow, [ctx.agentRoot, options.force === true]);
12
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Eve-aligned Blob bootstrap for mcp-radar on Vercel.
4
+ * Links a public Blob store to the project (OIDC at runtime — no manual tokens).
5
+ */
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ function run(command, args) {
9
+ const result = spawnSync(command, args, { stdio: "inherit" });
10
+ if (result.status !== 0) {
11
+ process.exit(result.status ?? 1);
12
+ }
13
+ }
14
+
15
+ console.log("Linking Vercel Blob store (OIDC, public)…");
16
+ run("vercel", ["blob", "create-store", "mcp-radar-proof", "--access", "public", "--yes"]);
17
+
18
+ console.log("Done. Redeploy so production picks up linked Blob storage.");
@@ -0,0 +1,26 @@
1
+ function workflowDispatchUrl(): string {
2
+ const explicit = process.env.PLASM_WORKFLOW_DISPATCH_URL?.trim();
3
+ if (explicit) return explicit;
4
+ const vercelUrl = process.env.VERCEL_URL?.trim();
5
+ if (vercelUrl) return `https://${vercelUrl}/internal/workflow/dispatch`;
6
+ const port = process.env.PORT ?? "3000";
7
+ return `http://127.0.0.1:${port}/internal/workflow/dispatch`;
8
+ }
9
+
10
+ export async function mcpRadarScanWorkflow(_agentRoot: string, force = false) {
11
+ "use workflow";
12
+ return mcpRadarScanStep(force);
13
+ }
14
+
15
+ async function mcpRadarScanStep(force: boolean) {
16
+ "use step";
17
+ const response = await fetch(workflowDispatchUrl(), {
18
+ method: "POST",
19
+ headers: { "content-type": "application/json" },
20
+ body: JSON.stringify({ job: "mcp-radar-scan", force }),
21
+ });
22
+ if (!response.ok) {
23
+ throw new Error(`workflow dispatch failed: ${response.status} ${await response.text()}`);
24
+ }
25
+ return response.json();
26
+ }