@plasm_lang/vercel-agent 0.3.111 → 0.3.113
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/package.json +5 -4
- package/src/archive/prod-archive-store.ts +3 -2
- package/src/instrumentation.ts +8 -13
- package/src/load-env.ts +50 -2
- package/src/nitro/create-plasm-nitro.ts +5 -0
- package/src/nitro/write-workflow-dispatch-route.ts +2 -1
- package/src/runtime/agent-runtime.ts +4 -0
- package/src/server/plasm-handler.ts +12 -6
- package/src/server/resolve-app-options.ts +33 -0
- package/src/stubs/plasm-value-emitter.ts +21 -0
- package/src/stubs/stub-symbols.ts +34 -0
- package/templates/mcp-radar/README.md +11 -6
- package/templates/mcp-radar/agent/channels/mcp-radar.ts +21 -14
- package/templates/mcp-radar/agent/hooks/proof-audit.ts +16 -2
- package/templates/mcp-radar/agent/instructions.md +23 -11
- package/templates/mcp-radar/agent/instrumentation.ts +3 -4
- package/templates/mcp-radar/agent/skills/proof-publish.md +15 -0
- package/templates/mcp-radar/lib/hn-algolia-preflight.ts +42 -0
- package/templates/mcp-radar/lib/instructions.md +36 -0
- package/templates/mcp-radar/lib/mcp-radar-scan.ts +28 -0
- package/templates/mcp-radar/lib/mcp-radar.ts +92 -0
- package/templates/mcp-radar/lib/proof-extract.ts +43 -0
- package/templates/mcp-radar/lib/proof-store.ts +38 -0
- package/templates/mcp-radar/lib/provision-vercel.mjs +98 -0
- package/templates/mcp-radar/lib/radar-state.ts +53 -0
- package/templates/mcp-radar/lib/run-audit.ts +41 -0
- package/templates/mcp-radar/lib/run-radar.ts +37 -139
- package/templates/mcp-radar/lib/smoke-channel.ts +42 -0
- package/templates/mcp-radar/lib/start-mcp-radar.ts +1 -0
- package/templates/mcp-radar/lib/test-hn-algolia-preflight.mjs +27 -0
- package/templates/mcp-radar/package.json +21 -3
- package/templates/mcp-radar/scripts/provision-vercel.mjs +111 -0
- package/templates/mcp-radar/scripts/test-hn-algolia-preflight.mjs +27 -0
- package/templates/mcp-radar/scripts/test-proof-extract.mjs +24 -0
- package/templates/mcp-radar/workflows/mcp-radar-scan.ts +4 -4
- package/templates/scaffold/agent/instrumentation.ts +3 -11
|
@@ -1,214 +1,112 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AuthoringContext,
|
|
3
|
-
} from "@plasm_lang/vercel-agent";
|
|
1
|
+
import { type AuthoringContext } from "@plasm_lang/vercel-agent";
|
|
4
2
|
|
|
3
|
+
import { drainRunAudit, resetRunAudit } from "./run-audit.js";
|
|
5
4
|
import {
|
|
6
|
-
addSeenIds,
|
|
7
|
-
appendProofRun,
|
|
8
5
|
gatewayConfigured,
|
|
9
6
|
loadLastRun,
|
|
10
|
-
loadSeenState,
|
|
11
7
|
saveLastRun,
|
|
12
|
-
saveSeenState,
|
|
13
8
|
tavilyConfigured,
|
|
14
|
-
} from "./
|
|
9
|
+
} from "./radar-state.js";
|
|
15
10
|
|
|
11
|
+
/** Stable intent — same string on every radar turn (`plasm_context` session reuse). */
|
|
16
12
|
export const MCP_RADAR_INTENT =
|
|
17
13
|
"track MCP innovations from Hacker News and corroborate with Tavily web search";
|
|
18
14
|
|
|
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
15
|
export interface RadarRunOptions {
|
|
28
16
|
force?: boolean;
|
|
17
|
+
reset?: boolean;
|
|
29
18
|
}
|
|
30
19
|
|
|
31
20
|
export interface RadarRunResult {
|
|
32
21
|
ok: boolean;
|
|
33
22
|
skipped: boolean;
|
|
34
23
|
reason?: string;
|
|
35
|
-
candidates: HnStoryCandidate[];
|
|
36
|
-
newCandidates: HnStoryCandidate[];
|
|
37
24
|
agentText?: string;
|
|
38
25
|
error?: string;
|
|
39
26
|
}
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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,
|
|
28
|
+
function buildRadarGoal(options: RadarRunOptions): string {
|
|
29
|
+
const lines = [
|
|
30
|
+
"Run one MCP radar scan cycle.",
|
|
87
31
|
"",
|
|
88
|
-
|
|
32
|
+
"Use the Plasm tool loop only: `plasm_context` → `plasm` → `plasm_run`.",
|
|
33
|
+
"Follow `agent/instructions.md` and skills `mcp-proof-format`, `proof-publish`.",
|
|
34
|
+
"HN search, Tavily corroboration, and Proof document read/write are Plasm programs in your session — not TypeScript, not stubs, not chat-only markdown.",
|
|
89
35
|
"",
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
36
|
+
`Reuse this intent on every plasm_context call: ${JSON.stringify(MCP_RADAR_INTENT)}`,
|
|
37
|
+
];
|
|
38
|
+
if (options.force === true) {
|
|
39
|
+
lines.push("", "Force: include MCP HN stories even if the Proof doc already mentions them.");
|
|
40
|
+
}
|
|
41
|
+
if (options.reset === true) {
|
|
42
|
+
lines.push("", "Reset: clear the Proof document to `# MCP Innovations Proof Log` before new entries.");
|
|
43
|
+
}
|
|
44
|
+
return lines.join("\n");
|
|
94
45
|
}
|
|
95
46
|
|
|
47
|
+
/** Start the agent loop; all catalog work happens inside Plasm session tools. */
|
|
96
48
|
export async function runRadar(
|
|
97
49
|
ctx: AuthoringContext,
|
|
98
50
|
options: RadarRunOptions = {},
|
|
99
51
|
): 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
52
|
if (!gatewayConfigured()) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
skipped: true,
|
|
134
|
-
reason: "ai_gateway_missing",
|
|
135
|
-
candidates,
|
|
136
|
-
newCandidates,
|
|
137
|
-
error: "AI Gateway is not configured (link project on Vercel or set AI_GATEWAY_API_KEY locally)",
|
|
138
|
-
};
|
|
53
|
+
const error =
|
|
54
|
+
"AI Gateway is not configured (link project on Vercel or set AI_GATEWAY_API_KEY locally)";
|
|
139
55
|
await saveLastRun(ctx.agentRoot, {
|
|
140
56
|
at: new Date().toISOString(),
|
|
141
57
|
status: "error",
|
|
142
|
-
|
|
143
|
-
message: result.error,
|
|
58
|
+
message: error,
|
|
144
59
|
});
|
|
145
|
-
return
|
|
60
|
+
return { ok: false, skipped: true, reason: "ai_gateway_missing", error };
|
|
146
61
|
}
|
|
147
62
|
|
|
148
|
-
const toProcess = newCandidates.length ? newCandidates : candidates;
|
|
149
|
-
const goal = buildAgentGoal(toProcess, tavilyConfigured());
|
|
150
|
-
|
|
151
63
|
try {
|
|
64
|
+
resetRunAudit();
|
|
152
65
|
const agent = await ctx.getAgent();
|
|
153
|
-
const turn = await agent.generate(
|
|
66
|
+
const turn = await agent.generate(buildRadarGoal(options), { resetConversation: false });
|
|
154
67
|
const runAt = new Date().toISOString();
|
|
68
|
+
const audit = drainRunAudit();
|
|
155
69
|
|
|
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
70
|
await saveLastRun(ctx.agentRoot, {
|
|
169
71
|
at: runAt,
|
|
170
72
|
status: "ok",
|
|
171
|
-
|
|
73
|
+
runIds: audit.runIds,
|
|
74
|
+
logicalSessionRef: audit.logicalSessionRef,
|
|
172
75
|
});
|
|
173
76
|
|
|
174
77
|
return {
|
|
175
78
|
ok: true,
|
|
176
79
|
skipped: false,
|
|
177
|
-
candidates,
|
|
178
|
-
newCandidates: toProcess,
|
|
179
80
|
agentText: turn.text,
|
|
180
81
|
};
|
|
181
82
|
} catch (err) {
|
|
182
83
|
const message = String(err);
|
|
183
|
-
await saveSeenState(ctx.agentRoot, {
|
|
184
|
-
...(await loadSeenState(ctx.agentRoot)),
|
|
185
|
-
lastRunAt: new Date().toISOString(),
|
|
186
|
-
lastRunStatus: "error",
|
|
187
|
-
});
|
|
188
84
|
await saveLastRun(ctx.agentRoot, {
|
|
189
85
|
at: new Date().toISOString(),
|
|
190
86
|
status: "error",
|
|
191
|
-
newItems: 0,
|
|
192
87
|
message,
|
|
193
88
|
});
|
|
194
89
|
return {
|
|
195
90
|
ok: false,
|
|
196
91
|
skipped: false,
|
|
197
|
-
candidates,
|
|
198
|
-
newCandidates: toProcess,
|
|
199
92
|
error: message,
|
|
200
93
|
};
|
|
201
94
|
}
|
|
202
95
|
}
|
|
203
96
|
|
|
204
97
|
export async function radarStatus(agentRoot: string): Promise<Record<string, unknown>> {
|
|
205
|
-
const seen = await loadSeenState(agentRoot);
|
|
206
98
|
const last = await loadLastRun(agentRoot);
|
|
207
99
|
return {
|
|
208
100
|
gateway: gatewayConfigured(),
|
|
209
101
|
tavily: tavilyConfigured(),
|
|
210
|
-
seenCount: seen.itemIds.length,
|
|
211
102
|
lastRun: last,
|
|
212
103
|
intent: MCP_RADAR_INTENT,
|
|
104
|
+
observability: {
|
|
105
|
+
operatorRunsUrl: "/operator/runs",
|
|
106
|
+
operatorSessionsUrl: "/operator/sessions",
|
|
107
|
+
operatorArchivesUrl: "/operator/archives",
|
|
108
|
+
runIds: last?.runIds ?? [],
|
|
109
|
+
logicalSessionRef: last?.logicalSessionRef,
|
|
110
|
+
},
|
|
213
111
|
};
|
|
214
112
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Smoke: MCP Radar channel routes (status, proof link, run with gateway check).
|
|
4
|
+
*/
|
|
5
|
+
import { createPlasmDevServer } from "@plasm_lang/vercel-agent/dev";
|
|
6
|
+
|
|
7
|
+
import { gatewayConfigured } from "../lib/radar-state.js";
|
|
8
|
+
|
|
9
|
+
async function main(): Promise<void> {
|
|
10
|
+
const handle = await createPlasmDevServer({ port: 0 });
|
|
11
|
+
try {
|
|
12
|
+
const statusRes = await fetch(`${handle.url}/channel/mcp-radar/status`);
|
|
13
|
+
if (!statusRes.ok) throw new Error(`status ${statusRes.status}`);
|
|
14
|
+
const status = (await statusRes.json()) as { gateway?: boolean; intent?: string };
|
|
15
|
+
if (status.intent == null) throw new Error("status missing intent");
|
|
16
|
+
|
|
17
|
+
const proofRes = await fetch(`${handle.url}/channel/mcp-radar/proof`);
|
|
18
|
+
if (proofRes.status === 404) {
|
|
19
|
+
console.log("OK: proof route (not configured locally)");
|
|
20
|
+
} else if (!proofRes.ok) {
|
|
21
|
+
throw new Error(`proof ${proofRes.status}`);
|
|
22
|
+
} else {
|
|
23
|
+
const proof = (await proofRes.json()) as { editor?: string };
|
|
24
|
+
if (proof.editor !== "proof") throw new Error("proof response missing editor=proof");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (gatewayConfigured()) {
|
|
28
|
+
console.log("OK: gateway configured for agent runs");
|
|
29
|
+
} else {
|
|
30
|
+
console.log("SKIP: gateway not configured locally");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`OK: mcp-radar channel smoke (${handle.url})`);
|
|
34
|
+
} finally {
|
|
35
|
+
await handle.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
main().catch((err: unknown) => {
|
|
40
|
+
console.error(err);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { fetchHnMcpStoriesByDate } from "../lib/hn-algolia-preflight.js";
|
|
5
|
+
import { isMcpRelevant } from "../lib/proof-extract.js";
|
|
6
|
+
import { MCP_SEARCH_QUERY, preflightHnMcpStories } from "../lib/run-radar.js";
|
|
7
|
+
|
|
8
|
+
const rows = await fetchHnMcpStoriesByDate({
|
|
9
|
+
query: MCP_SEARCH_QUERY,
|
|
10
|
+
tags: "story",
|
|
11
|
+
perPage: 10,
|
|
12
|
+
});
|
|
13
|
+
assert.ok(rows.length > 0, "Algolia should return hits for MCP query");
|
|
14
|
+
|
|
15
|
+
const mcpRows = rows.filter((r) => isMcpRelevant(r.title, r.url));
|
|
16
|
+
assert.ok(mcpRows.length > 0, "At least one hit should be MCP-relevant");
|
|
17
|
+
assert.equal(
|
|
18
|
+
mcpRows.some((r) => /hawking|altman|crowdstrike/i.test(r.title ?? "")),
|
|
19
|
+
false,
|
|
20
|
+
"Preflight must not return generic top HN stories",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const candidates = await preflightHnMcpStories();
|
|
24
|
+
assert.ok(candidates.length > 0, "preflightHnMcpStories should yield MCP candidates");
|
|
25
|
+
console.log(
|
|
26
|
+
`hn-algolia-preflight: ok (${candidates.length} MCP candidates, first: ${candidates[0]?.title?.slice(0, 60)})`,
|
|
27
|
+
);
|
|
@@ -4,11 +4,29 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Event-driven MCP innovation radar — Hacker News + Tavily proof log",
|
|
6
6
|
"scripts": {
|
|
7
|
+
"build": "plasm-agent build",
|
|
8
|
+
"dev": "plasm-agent dev",
|
|
9
|
+
"dev:interactive": "plasm-agent dev --interactive",
|
|
10
|
+
"info": "plasm-agent info",
|
|
11
|
+
"deploy": "vercel deploy",
|
|
12
|
+
"eval": "tsx scripts/run-evals.ts",
|
|
7
13
|
"smoke:channel": "tsx scripts/smoke-channel.ts",
|
|
8
|
-
"
|
|
14
|
+
"test:proof-extract": "tsx scripts/test-proof-extract.mjs",
|
|
15
|
+
"test:hn-preflight": "tsx scripts/test-hn-algolia-preflight.mjs"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@ai-sdk/otel": "1.0.14",
|
|
19
|
+
"@plasm_lang/engine": "0.3.112",
|
|
20
|
+
"@plasm_lang/vercel-agent": "file:../../plasm-oss/packages/plasm-agent",
|
|
21
|
+
"@vercel/blob": "0.27.3",
|
|
22
|
+
"@vercel/functions": "3.7.4",
|
|
23
|
+
"@vercel/otel": "1.14.2",
|
|
24
|
+
"ai": "7.0.14",
|
|
25
|
+
"esbuild": "0.25.12",
|
|
26
|
+
"workflow": "4.5.0",
|
|
27
|
+
"zod": "4.3.6"
|
|
9
28
|
},
|
|
10
|
-
"dependencies": {},
|
|
11
29
|
"devDependencies": {
|
|
12
|
-
"tsx": "
|
|
30
|
+
"tsx": "4.19.4"
|
|
13
31
|
}
|
|
14
32
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Eve-aligned Vercel bootstrap for mcp-radar:
|
|
4
|
+
* - Sync secrets from monorepo `.env` → Vercel project env
|
|
5
|
+
* - Link Blob store (OIDC at runtime)
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const projectRoot = path.resolve(scriptDir, "..");
|
|
14
|
+
|
|
15
|
+
function run(command, args, options = {}) {
|
|
16
|
+
const result = spawnSync(command, args, options);
|
|
17
|
+
if (result.status !== 0) {
|
|
18
|
+
process.exit(result.status ?? 1);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseEnvFile(filePath) {
|
|
24
|
+
const vars = new Map();
|
|
25
|
+
if (!existsSync(filePath)) return vars;
|
|
26
|
+
const text = readFileSync(filePath, "utf8");
|
|
27
|
+
for (const line of text.split(/\r?\n/)) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
30
|
+
const withoutExport = trimmed.startsWith("export ")
|
|
31
|
+
? trimmed.slice("export ".length).trim()
|
|
32
|
+
: trimmed;
|
|
33
|
+
const eq = withoutExport.indexOf("=");
|
|
34
|
+
if (eq <= 0) continue;
|
|
35
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
36
|
+
let value = withoutExport.slice(eq + 1).trim();
|
|
37
|
+
if (
|
|
38
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
39
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
40
|
+
) {
|
|
41
|
+
value = value.slice(1, -1);
|
|
42
|
+
}
|
|
43
|
+
vars.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
return vars;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveMonorepoEnv() {
|
|
49
|
+
const files = [];
|
|
50
|
+
let dir = projectRoot;
|
|
51
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
52
|
+
const candidate = path.join(dir, ".env");
|
|
53
|
+
if (existsSync(candidate)) files.push(candidate);
|
|
54
|
+
const parent = path.dirname(dir);
|
|
55
|
+
if (parent === dir) break;
|
|
56
|
+
dir = parent;
|
|
57
|
+
}
|
|
58
|
+
const merged = new Map();
|
|
59
|
+
for (const file of files.reverse()) {
|
|
60
|
+
for (const [key, value] of parseEnvFile(file)) {
|
|
61
|
+
if (value.trim()) merged.set(key, value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function envAlreadySet(name) {
|
|
68
|
+
const result = run("vercel", ["env", "ls"], { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] });
|
|
69
|
+
return (result.stdout ?? "").includes(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function syncEnvVar(name, value) {
|
|
73
|
+
if (!value?.trim()) {
|
|
74
|
+
console.warn(`[provision] skip ${name}: not found in monorepo .env`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (envAlreadySet(name)) {
|
|
78
|
+
console.log(`[provision] ${name} already set on Vercel — skipping`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const target of ["production", "preview"]) {
|
|
82
|
+
console.log(`[provision] adding ${name} → ${target}…`);
|
|
83
|
+
run("vercel", ["env", "add", name, target], {
|
|
84
|
+
input: value,
|
|
85
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const env = resolveMonorepoEnv();
|
|
91
|
+
|
|
92
|
+
console.log("Syncing env from monorepo .env → Vercel project…");
|
|
93
|
+
syncEnvVar("TAVILY_API_TOKEN", env.get("TAVILY_API_TOKEN"));
|
|
94
|
+
syncEnvVar("PLASM_TENANT_SCOPE", env.get("PLASM_TENANT_SCOPE") ?? "mcp-radar");
|
|
95
|
+
|
|
96
|
+
console.log("Linking Vercel Blob store (OIDC, public)…");
|
|
97
|
+
const blobResult = spawnSync(
|
|
98
|
+
"vercel",
|
|
99
|
+
["blob", "create-store", "mcp-radar-proof", "--access", "public", "--yes"],
|
|
100
|
+
{ encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] },
|
|
101
|
+
);
|
|
102
|
+
if (blobResult.status !== 0) {
|
|
103
|
+
const err = blobResult.stderr ?? blobResult.stdout ?? "";
|
|
104
|
+
if (err.includes("already exists") || err.includes("(409)")) {
|
|
105
|
+
console.log("[provision] Blob store mcp-radar-proof already linked — skipping");
|
|
106
|
+
} else {
|
|
107
|
+
process.exit(blobResult.status ?? 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log("Done. Redeploy production so env + Blob bindings take effect.");
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { fetchHnMcpStoriesByDate } from "../lib/hn-algolia-preflight.js";
|
|
5
|
+
import { isMcpRelevant } from "../lib/proof-extract.js";
|
|
6
|
+
import { MCP_SEARCH_QUERY, preflightHnMcpStories } from "../lib/run-radar.js";
|
|
7
|
+
|
|
8
|
+
const rows = await fetchHnMcpStoriesByDate({
|
|
9
|
+
query: MCP_SEARCH_QUERY,
|
|
10
|
+
tags: "story",
|
|
11
|
+
perPage: 10,
|
|
12
|
+
});
|
|
13
|
+
assert.ok(rows.length > 0, "Algolia should return hits for MCP query");
|
|
14
|
+
|
|
15
|
+
const mcpRows = rows.filter((r) => isMcpRelevant(r.title, r.url));
|
|
16
|
+
assert.ok(mcpRows.length > 0, "At least one hit should be MCP-relevant");
|
|
17
|
+
assert.equal(
|
|
18
|
+
mcpRows.some((r) => /hawking|altman|crowdstrike/i.test(r.title ?? "")),
|
|
19
|
+
false,
|
|
20
|
+
"Preflight must not return generic top HN stories",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const candidates = await preflightHnMcpStories();
|
|
24
|
+
assert.ok(candidates.length > 0, "preflightHnMcpStories should yield MCP candidates");
|
|
25
|
+
console.log(
|
|
26
|
+
`hn-algolia-preflight: ok (${candidates.length} MCP candidates, first: ${candidates[0]?.title?.slice(0, 60)})`,
|
|
27
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { extractProofMarkdown, isMcpRelevant } from "../lib/proof-extract.js";
|
|
4
|
+
|
|
5
|
+
const raw = `
|
|
6
|
+
The session expired after plasm_context. Tavily is unavailable.
|
|
7
|
+
|
|
8
|
+
### [MCP Server Guide](https://news.ycombinator.com/item?id=123)
|
|
9
|
+
- **HN**: id 123, score 42
|
|
10
|
+
- **Synthesis**: Example MCP tooling.
|
|
11
|
+
- **Confidence**: low
|
|
12
|
+
|
|
13
|
+
Some trailing narration about errors.
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const extracted = extractProofMarkdown(raw);
|
|
17
|
+
assert.match(extracted, /^### \[MCP Server Guide\]/);
|
|
18
|
+
assert.doesNotMatch(extracted, /session expired/i);
|
|
19
|
+
assert.doesNotMatch(extracted, /trailing narration/i);
|
|
20
|
+
|
|
21
|
+
assert.equal(isMcpRelevant("New MCP server for agents", undefined), true);
|
|
22
|
+
assert.equal(isMcpRelevant("Stephen Hawking has died", undefined), false);
|
|
23
|
+
|
|
24
|
+
console.log("proof-extract: ok");
|
|
@@ -9,17 +9,17 @@ function workflowDispatchUrl(): string {
|
|
|
9
9
|
return `http://127.0.0.1:${port}/internal/workflow/dispatch`;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export async function mcpRadarScanWorkflow(_agentRoot: string, force = false) {
|
|
12
|
+
export async function mcpRadarScanWorkflow(_agentRoot: string, force = false, reset = false) {
|
|
13
13
|
"use workflow";
|
|
14
|
-
return mcpRadarScanStep(force);
|
|
14
|
+
return mcpRadarScanStep(force, reset);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async function mcpRadarScanStep(force: boolean) {
|
|
17
|
+
async function mcpRadarScanStep(force: boolean, reset: boolean) {
|
|
18
18
|
"use step";
|
|
19
19
|
const response = await fetch(workflowDispatchUrl(), {
|
|
20
20
|
method: "POST",
|
|
21
21
|
headers: { "content-type": "application/json" },
|
|
22
|
-
body: JSON.stringify({ job: "mcp-radar-scan", force }),
|
|
22
|
+
body: JSON.stringify({ job: "mcp-radar-scan", force, reset }),
|
|
23
23
|
});
|
|
24
24
|
if (!response.ok) {
|
|
25
25
|
throw new Error(`workflow dispatch failed: ${response.status} ${await response.text()}`);
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
|
|
4
1
|
import { registerOTel } from "@vercel/otel";
|
|
5
2
|
import { OpenTelemetry } from "@ai-sdk/otel";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const agentRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const serviceName =
|
|
10
|
-
process.env.PLASM_AGENT_NAME?.trim() || path.basename(path.dirname(agentRoot));
|
|
3
|
+
import { registerTelemetry } from "ai";
|
|
11
4
|
|
|
12
|
-
/** Eve-shaped OTEL bootstrap — optional export to Braintrust/Datadog via registerOTel. */
|
|
13
5
|
export function register(): void {
|
|
14
|
-
registerOTel({ serviceName });
|
|
15
|
-
|
|
6
|
+
registerOTel({ serviceName: "plasm-agent" });
|
|
7
|
+
registerTelemetry(new OpenTelemetry());
|
|
16
8
|
}
|
|
17
9
|
|
|
18
10
|
register();
|