@plasm_lang/vercel-agent 0.3.85 → 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.
- package/README.md +8 -10
- package/package.json +2 -2
- package/src/archive/adapters.ts +4 -0
- package/src/archive/prod-archive-store.ts +28 -45
- package/src/archive/resolve-backend.ts +8 -26
- package/src/archive/types.ts +1 -0
- package/src/archive/vercel-blob-adapter.ts +5 -0
- package/src/authoring/schedule-manager.ts +36 -22
- package/src/cli/build-manifest.ts +18 -0
- package/src/cli/build.ts +4 -13
- package/src/cli/compile-authored-slots.ts +9 -0
- package/src/cli/init-scaffold.ts +6 -2
- package/src/cli/init.ts +2 -6
- package/src/index.ts +3 -2
- package/src/nitro/agent-summary.ts +7 -7
- package/src/nitro/build-application.ts +43 -0
- package/src/nitro/create-plasm-nitro.ts +16 -2
- package/src/nitro/ensure-workflow-builder-dirs.ts +42 -0
- package/src/nitro/patch-vercel-config.ts +0 -3
- package/src/nitro/prepare-host.ts +5 -6
- package/src/nitro/workflow-directives.ts +1 -0
- package/src/nitro/write-nitro-schedule-tasks.ts +67 -0
- package/src/nitro/write-workflow-dispatch-route.ts +68 -0
- package/src/operator/routes.ts +6 -6
- package/src/project-info.ts +10 -6
- package/src/server/plasm-handler.ts +4 -4
- package/src/state/blob-state-adapter.ts +65 -0
- package/src/state/define-state.ts +10 -2
- package/src/state/fs-state-adapter.ts +1 -1
- package/src/storage/vercel-blob.ts +47 -0
- package/templates/mcp-radar/README.md +19 -16
- package/templates/mcp-radar/agent/channels/mcp-radar.ts +7 -7
- package/templates/mcp-radar/agent/schedules/mcp-radar-scan.ts +3 -5
- package/templates/mcp-radar/lib/proof-store.ts +43 -44
- package/templates/mcp-radar/lib/run-radar.ts +1 -1
- package/templates/mcp-radar/lib/start-mcp-radar.ts +12 -0
- package/templates/mcp-radar/scripts/provision-vercel-storage.mjs +18 -0
- 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
|
|
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
|
-
|
|
49
|
-
console.error("[mcp-radar]
|
|
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,
|
|
53
|
+
sendJson(res, 202, { accepted: true, workflow: true });
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
+
}
|