@tokagent/tokagentos 2.0.24 → 2.0.30

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 (26) hide show
  1. package/package.json +1 -1
  2. package/scaffold-patches/packages/agent/src/api/plugin-routes.ts +1889 -0
  3. package/scaffold-patches/packages/agent/src/api/server.ts +4509 -0
  4. package/scaffold-patches/packages/agent/src/api/trigger-routes.ts +942 -0
  5. package/scaffold-patches/packages/agent/src/runtime/core-plugins.ts +4 -0
  6. package/scaffold-patches/packages/agent/src/triggers/runtime.ts +955 -0
  7. package/scaffold-patches/packages/app-core/src/api/automations-compat-routes.ts +924 -0
  8. package/scaffold-patches/packages/app-core/src/api/client-agent.ts +2755 -0
  9. package/scaffold-patches/packages/app-core/src/components/pages/AutomationsView.tsx +446 -26
  10. package/scaffold-patches/packages/app-core/src/components/pages/SettingsView.tsx +155 -0
  11. package/scaffold-patches/packages/shared/src/onboarding-presets.characters.ts +16 -16
  12. package/templates/fullstack-app/package.json +9 -5
  13. package/templates/fullstack-app/plugins/plugin-tokagent-billing/package.json +1 -1
  14. package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/__tests__/routes/estimate-routes.test.ts +5 -2
  15. package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/dashboard/app.js +896 -19
  16. package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/dashboard/index.html +280 -94
  17. package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/dashboard/style.css +969 -235
  18. package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/keys-routes.ts +170 -0
  19. package/templates/fullstack-app/plugins/plugin-tokagent-billing/src/routes/messages-proxy-routes.ts +114 -3
  20. package/templates/fullstack-app/plugins/plugin-web-fetch/build.ts +35 -0
  21. package/templates/fullstack-app/plugins/plugin-web-fetch/package.json +37 -0
  22. package/templates/fullstack-app/plugins/plugin-web-fetch/src/index.ts +471 -0
  23. package/templates/fullstack-app/plugins/plugin-web-fetch/tsconfig.json +20 -0
  24. package/templates/fullstack-app/scripts/ensure-plugin-builds.mjs +1 -0
  25. package/templates/fullstack-app/scripts/verify-llm-plugins.mjs +122 -0
  26. package/templates-manifest.json +1 -1
@@ -0,0 +1,471 @@
1
+ /**
2
+ * @tokagent/plugin-web-fetch
3
+ *
4
+ * Two actions for letting the agent reach the live web:
5
+ *
6
+ * FETCH_URL — retrieve a single http(s) URL's raw content using Node's
7
+ * built-in fetch. No external service, no API key. Best
8
+ * for static HTML, JSON APIs, RSS feeds. No JS rendering.
9
+ *
10
+ * WEB_SEARCH — search the entire web for a query, return ranked
11
+ * results with snippets. Backed by Tavily's HTTP API
12
+ * (the same backend elizaOS's `plugin-web-search` uses).
13
+ * Requires TAVILY_API_KEY in env (free tier: 1k/month at
14
+ * https://app.tavily.com/sign-in). Cleanly errors when
15
+ * the key is missing — never silently no-ops.
16
+ *
17
+ * Output is truncated to MAX_BYTES so a giant response can't blow the
18
+ * LLM's context window. Both actions have a defensive timeout so the
19
+ * autonomy loop stays responsive.
20
+ */
21
+
22
+ import type {
23
+ Action,
24
+ Content,
25
+ HandlerCallback,
26
+ IAgentRuntime,
27
+ Memory,
28
+ Plugin,
29
+ State,
30
+ } from "@elizaos/core";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Tunables
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const FETCH_TIMEOUT_MS = 30_000;
37
+ const MAX_BYTES = 16_000;
38
+ const USER_AGENT =
39
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 " +
40
+ "(KHTML, like Gecko) tokagent-web-fetch/0.1.0 Safari/537.36";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // URL extraction (shared)
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const URL_PATTERN = /\bhttps?:\/\/[^\s<>"'`)]+/i;
47
+
48
+ function extractUrlFromOptions(
49
+ options: Record<string, unknown> | undefined,
50
+ ): string | null {
51
+ if (!options) return null;
52
+ const direct = options.url ?? options.URL ?? options.target;
53
+ if (typeof direct === "string" && URL_PATTERN.test(direct)) {
54
+ return direct.match(URL_PATTERN)?.[0] ?? null;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ function extractUrlFromMessage(message: Memory): string | null {
60
+ const text =
61
+ typeof message.content?.text === "string" ? message.content.text : "";
62
+ if (!text) return null;
63
+ return text.match(URL_PATTERN)?.[0] ?? null;
64
+ }
65
+
66
+ // ===========================================================================
67
+ // FETCH_URL
68
+ // ===========================================================================
69
+
70
+ export const fetchUrlAction: Action = {
71
+ name: "FETCH_URL",
72
+ similes: [
73
+ "WEB_FETCH",
74
+ "GET_URL",
75
+ "DOWNLOAD_URL",
76
+ "FETCH_PAGE",
77
+ "READ_URL",
78
+ "READ_WEB_PAGE",
79
+ "RETRIEVE_URL",
80
+ ],
81
+ description:
82
+ "Retrieve the raw body of an http(s) URL. Use whenever the user asks " +
83
+ "you to read, summarize, check, or fetch ANY web content identified " +
84
+ "by a URL — RSS feeds, JSON APIs, blog posts, news pages, status " +
85
+ "pages, etc. Cannot execute JavaScript. Output is truncated to 16 KB.",
86
+
87
+ validate: async (
88
+ _runtime: IAgentRuntime,
89
+ message: Memory,
90
+ _state?: State,
91
+ ): Promise<boolean> => {
92
+ const text =
93
+ typeof message.content?.text === "string" ? message.content.text : "";
94
+ if (URL_PATTERN.test(text)) return true;
95
+ return /\b(fetch|download|retrieve|read|scrape|grab|pull|crawl|check)\b.*\b(url|link|page|site|feed|rss|api|endpoint)\b/i.test(
96
+ text,
97
+ );
98
+ },
99
+
100
+ handler: async (
101
+ _runtime: IAgentRuntime,
102
+ message: Memory,
103
+ _state?: State,
104
+ options?: Record<string, unknown>,
105
+ callback?: HandlerCallback,
106
+ ): Promise<undefined> => {
107
+ const url = extractUrlFromOptions(options) ?? extractUrlFromMessage(message);
108
+ if (!url) {
109
+ await callback?.({
110
+ text:
111
+ "FETCH_URL requires a target URL. Include the http(s):// URL in " +
112
+ "the instruction text, or pass it via the action's `url` option.",
113
+ action: "FETCH_URL",
114
+ source: "web-fetch",
115
+ } as Content);
116
+ return undefined;
117
+ }
118
+
119
+ const controller = new AbortController();
120
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
121
+ try {
122
+ const resp = await fetch(url, {
123
+ method: "GET",
124
+ redirect: "follow",
125
+ headers: {
126
+ "User-Agent": USER_AGENT,
127
+ Accept:
128
+ "text/html,application/xhtml+xml,application/xml;q=0.9," +
129
+ "application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5",
130
+ },
131
+ signal: controller.signal,
132
+ });
133
+
134
+ const contentType = resp.headers.get("content-type") ?? "unknown";
135
+ const status = resp.status;
136
+ const raw = await resp.text();
137
+ const byteLength = Buffer.byteLength(raw, "utf8");
138
+ const body =
139
+ byteLength > MAX_BYTES
140
+ ? `${raw.slice(0, MAX_BYTES)}\n…[truncated — original size ${byteLength} bytes]`
141
+ : raw;
142
+
143
+ await callback?.({
144
+ text:
145
+ `Fetched ${url}\n` +
146
+ `Status: ${status} Content-Type: ${contentType} Size: ${byteLength} bytes\n\n` +
147
+ body,
148
+ action: "FETCH_URL",
149
+ source: "web-fetch",
150
+ url,
151
+ } as Content);
152
+ } catch (err) {
153
+ const message =
154
+ err instanceof Error && err.name === "AbortError"
155
+ ? `timed out after ${FETCH_TIMEOUT_MS}ms`
156
+ : err instanceof Error
157
+ ? err.message
158
+ : String(err);
159
+ await callback?.({
160
+ text: `FETCH_URL failed for ${url}: ${message}`,
161
+ action: "FETCH_URL",
162
+ source: "web-fetch",
163
+ } as Content);
164
+ } finally {
165
+ clearTimeout(timer);
166
+ }
167
+ return undefined;
168
+ },
169
+
170
+ examples: [
171
+ [
172
+ { name: "user", content: { text: "Fetch https://news.ycombinator.com/rss" } },
173
+ {
174
+ name: "agent",
175
+ content: { text: "Fetching the Hacker News RSS feed.", action: "FETCH_URL" },
176
+ },
177
+ ],
178
+ ],
179
+ };
180
+
181
+ // ===========================================================================
182
+ // WEB_SEARCH (Tavily-backed, matches elizaOS pattern)
183
+ // ===========================================================================
184
+
185
+ type TavilySearchDepth = "basic" | "advanced";
186
+
187
+ const TAVILY_DEFAULT_MAX_RESULTS = 5;
188
+ const TAVILY_MAX_RESULTS_CEILING = 20;
189
+ const TAVILY_TIMEOUT_MS = 30_000;
190
+ const TAVILY_ENDPOINT = "https://api.tavily.com/search";
191
+
192
+ interface TavilyResult {
193
+ title?: string;
194
+ url?: string;
195
+ content?: string;
196
+ score?: number;
197
+ published_date?: string;
198
+ }
199
+
200
+ interface TavilyResponse {
201
+ answer?: string;
202
+ query?: string;
203
+ results?: TavilyResult[];
204
+ response_time?: number;
205
+ }
206
+
207
+ function extractQueryFromOptions(
208
+ options: Record<string, unknown> | undefined,
209
+ ): string | null {
210
+ if (!options) return null;
211
+ const direct = options.query ?? options.q ?? options.search;
212
+ return typeof direct === "string" && direct.trim() ? direct.trim() : null;
213
+ }
214
+
215
+ function extractQueryFromMessage(message: Memory): string | null {
216
+ const text =
217
+ typeof message.content?.text === "string" ? message.content.text : "";
218
+ if (!text.trim()) return null;
219
+ const m = text.match(
220
+ /(?:search\s+(?:for|the\s+web\s+for)|find|look\s+up|google)\s+(?:about\s+)?["']?([^"'\n.?!]{3,200})/i,
221
+ );
222
+ if (m?.[1]) return m[1].trim();
223
+ return text.trim().slice(0, 400);
224
+ }
225
+
226
+ function clampMaxResults(value: unknown): number {
227
+ const n = typeof value === "number" ? value : Number(value);
228
+ if (!Number.isFinite(n) || n <= 0) return TAVILY_DEFAULT_MAX_RESULTS;
229
+ return Math.min(Math.floor(n), TAVILY_MAX_RESULTS_CEILING);
230
+ }
231
+
232
+ function formatTavilyResults(payload: TavilyResponse, query: string): string {
233
+ const lines: string[] = [];
234
+ lines.push(`Web search results for: "${query}"`);
235
+ if (typeof payload.response_time === "number") {
236
+ lines.push(`(response_time=${payload.response_time.toFixed(2)}s)`);
237
+ }
238
+ lines.push("");
239
+ if (payload.answer) {
240
+ lines.push("Synthesized answer:");
241
+ lines.push(payload.answer);
242
+ lines.push("");
243
+ }
244
+ const results = payload.results ?? [];
245
+ if (results.length === 0) {
246
+ lines.push("No results returned.");
247
+ } else {
248
+ lines.push(`Top ${results.length} results:`);
249
+ results.forEach((r, i) => {
250
+ lines.push("");
251
+ lines.push(`${i + 1}. ${r.title ?? "(untitled)"}`);
252
+ if (r.url) lines.push(` URL: ${r.url}`);
253
+ if (r.published_date) lines.push(` Published: ${r.published_date}`);
254
+ if (typeof r.score === "number") {
255
+ lines.push(` Relevance: ${r.score.toFixed(3)}`);
256
+ }
257
+ if (r.content) {
258
+ const snippet =
259
+ r.content.length > 600 ? `${r.content.slice(0, 600)}…` : r.content;
260
+ lines.push(` ${snippet}`);
261
+ }
262
+ });
263
+ }
264
+ const out = lines.join("\n");
265
+ return out.length > MAX_BYTES
266
+ ? `${out.slice(0, MAX_BYTES)}\n…[truncated]`
267
+ : out;
268
+ }
269
+
270
+ export const webSearchAction: Action = {
271
+ name: "WEB_SEARCH",
272
+ similes: [
273
+ "SEARCH_WEB",
274
+ "GOOGLE",
275
+ "SEARCH_THE_WEB",
276
+ "FIND_ONLINE",
277
+ "LOOK_UP_ONLINE",
278
+ "INTERNET_SEARCH",
279
+ "WEB_QUERY",
280
+ ],
281
+ description:
282
+ "Search the entire web for a query and return ranked results with " +
283
+ "snippets and a synthesized answer (when available). Use whenever the " +
284
+ "user asks you to find, search, look up, or discover information " +
285
+ "online without specifying a URL. Backed by Tavily — requires " +
286
+ "TAVILY_API_KEY in env.",
287
+
288
+ validate: async (
289
+ _runtime: IAgentRuntime,
290
+ message: Memory,
291
+ _state?: State,
292
+ ): Promise<boolean> => {
293
+ const text =
294
+ typeof message.content?.text === "string" ? message.content.text : "";
295
+ if (!text.trim()) return false;
296
+ return /\b(search|find|look\s*up|google|google\s+for|web|online|internet|news|trends?|latest|what.?s\s+new|discover)\b/i.test(
297
+ text,
298
+ );
299
+ },
300
+
301
+ handler: async (
302
+ runtime: IAgentRuntime,
303
+ message: Memory,
304
+ _state?: State,
305
+ options?: Record<string, unknown>,
306
+ callback?: HandlerCallback,
307
+ ): Promise<undefined> => {
308
+ // Server-side log breadcrumb so we can trace what the agent invoked
309
+ // even when the LLM paraphrases the result. Look for [WEB_SEARCH]
310
+ // lines in the agent boot/run log.
311
+ const log = (msg: string, extra?: Record<string, unknown>) => {
312
+ try {
313
+ // biome-ignore lint/suspicious/noConsole: intentional diagnostic
314
+ console.info(`[WEB_SEARCH] ${msg}`, extra ?? {});
315
+ } catch {
316
+ // ignore
317
+ }
318
+ };
319
+
320
+ const apiKey =
321
+ (typeof runtime.getSetting === "function"
322
+ ? runtime.getSetting("TAVILY_API_KEY")
323
+ : undefined) ?? process.env.TAVILY_API_KEY;
324
+ if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
325
+ log("missing TAVILY_API_KEY");
326
+ await callback?.({
327
+ text:
328
+ "WEB_SEARCH is not configured: TAVILY_API_KEY is missing. " +
329
+ "Get a free key at https://app.tavily.com/sign-in (1,000 " +
330
+ "searches/month, no credit card), then add " +
331
+ "`TAVILY_API_KEY=tvly-...` to your project's .env (or set it " +
332
+ "via Settings → Plugins → web-fetch) and restart the agent.",
333
+ action: "WEB_SEARCH",
334
+ source: "web-fetch",
335
+ } as Content);
336
+ return undefined;
337
+ }
338
+
339
+ const query =
340
+ extractQueryFromOptions(options) ?? extractQueryFromMessage(message);
341
+ if (!query) {
342
+ log("missing query");
343
+ await callback?.({
344
+ text:
345
+ "WEB_SEARCH requires a query. Include the search terms in the " +
346
+ "instruction or pass them via the action's `query` option.",
347
+ action: "WEB_SEARCH",
348
+ source: "web-fetch",
349
+ } as Content);
350
+ return undefined;
351
+ }
352
+
353
+ const searchDepth: TavilySearchDepth =
354
+ options?.searchDepth === "advanced" ? "advanced" : "basic";
355
+ const maxResults = clampMaxResults(options?.maxResults);
356
+ log("dispatching to Tavily", {
357
+ queryPreview: query.slice(0, 120),
358
+ searchDepth,
359
+ maxResults,
360
+ keyPrefix: apiKey.trim().slice(0, 8),
361
+ });
362
+
363
+ const controller = new AbortController();
364
+ const timer = setTimeout(() => controller.abort(), TAVILY_TIMEOUT_MS);
365
+ const startedAt = Date.now();
366
+ try {
367
+ const resp = await fetch(TAVILY_ENDPOINT, {
368
+ method: "POST",
369
+ headers: {
370
+ "Content-Type": "application/json",
371
+ "User-Agent": USER_AGENT,
372
+ },
373
+ body: JSON.stringify({
374
+ api_key: apiKey.trim(),
375
+ query,
376
+ search_depth: searchDepth,
377
+ max_results: maxResults,
378
+ include_answer: true,
379
+ }),
380
+ signal: controller.signal,
381
+ });
382
+
383
+ if (!resp.ok) {
384
+ const errBody = await resp.text().catch(() => "");
385
+ log("Tavily HTTP error", {
386
+ status: resp.status,
387
+ statusText: resp.statusText,
388
+ bodyPreview: errBody.slice(0, 300),
389
+ elapsedMs: Date.now() - startedAt,
390
+ });
391
+ const hint =
392
+ resp.status === 401 || resp.status === 403
393
+ ? " — verify TAVILY_API_KEY is correct."
394
+ : resp.status === 429
395
+ ? " — Tavily rate limit reached; check your usage dashboard."
396
+ : "";
397
+ await callback?.({
398
+ text: `WEB_SEARCH failed (${resp.status} ${resp.statusText})${hint}\n${errBody.slice(0, 400)}`,
399
+ action: "WEB_SEARCH",
400
+ source: "web-fetch",
401
+ } as Content);
402
+ return undefined;
403
+ }
404
+
405
+ const payload = (await resp.json()) as TavilyResponse;
406
+ const resultCount = payload.results?.length ?? 0;
407
+ log("Tavily ok", {
408
+ elapsedMs: Date.now() - startedAt,
409
+ resultCount,
410
+ hasAnswer: Boolean(payload.answer),
411
+ firstUrl: payload.results?.[0]?.url ?? null,
412
+ });
413
+ await callback?.({
414
+ text: formatTavilyResults(payload, query),
415
+ action: "WEB_SEARCH",
416
+ source: "web-fetch",
417
+ query,
418
+ } as Content);
419
+ } catch (err) {
420
+ const errMessage =
421
+ err instanceof Error && err.name === "AbortError"
422
+ ? `timed out after ${TAVILY_TIMEOUT_MS}ms`
423
+ : err instanceof Error
424
+ ? err.message
425
+ : String(err);
426
+ log("Tavily call threw", {
427
+ error: errMessage,
428
+ elapsedMs: Date.now() - startedAt,
429
+ });
430
+ await callback?.({
431
+ text: `WEB_SEARCH failed: ${errMessage}`,
432
+ action: "WEB_SEARCH",
433
+ source: "web-fetch",
434
+ } as Content);
435
+ } finally {
436
+ clearTimeout(timer);
437
+ }
438
+ return undefined;
439
+ },
440
+
441
+ examples: [
442
+ [
443
+ { name: "user", content: { text: "Find the latest AI trends online" } },
444
+ {
445
+ name: "agent",
446
+ content: {
447
+ text: "Searching the web for the latest AI trends.",
448
+ action: "WEB_SEARCH",
449
+ },
450
+ },
451
+ ],
452
+ ],
453
+ };
454
+
455
+ // ---------------------------------------------------------------------------
456
+ // Plugin definition
457
+ // ---------------------------------------------------------------------------
458
+
459
+ export const webFetchPlugin: Plugin = {
460
+ name: "web-fetch",
461
+ description:
462
+ "Two actions to reach the live web: FETCH_URL (Node fetch — static " +
463
+ "HTML/JSON/RSS, no key) and WEB_SEARCH (Tavily-backed search — " +
464
+ "requires TAVILY_API_KEY).",
465
+ actions: [fetchUrlAction, webSearchAction],
466
+ providers: [],
467
+ services: [],
468
+ evaluators: [],
469
+ };
470
+
471
+ export default webFetchPlugin;
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "allowImportingTsExtensions": true,
11
+ "noEmit": true,
12
+ "declaration": false,
13
+ "types": ["node"],
14
+ "paths": {
15
+ "@elizaos/core": ["../../tokagent/packages/typescript/src/index.node.ts"],
16
+ "@elizaos/core/*": ["../../tokagent/packages/typescript/src/*"]
17
+ }
18
+ },
19
+ "include": ["src/**/*.ts"]
20
+ }
@@ -174,6 +174,7 @@ const TOKAGENT_PLUGIN_PATHS = [
174
174
  "plugins/plugin-tokagent-polymarket",
175
175
  "plugins/plugin-tokagent-strategy",
176
176
  "plugins/plugin-tokagent-billing",
177
+ "plugins/plugin-web-fetch",
177
178
  ];
178
179
 
179
180
  /**
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Guard: verify installed LLM-provider plugin bundles are intact + API-compatible
4
+ * with the vendored `@elizaos/core` (tokagent/packages/typescript).
5
+ *
6
+ * Three failure modes have actually happened — this guard refuses to install
7
+ * or boot if any recur:
8
+ * 1. `@elizaos/plugin-openrouter` 2.0.0-alpha.11/12/13 ship a 79-line
9
+ * broken bundle (utility helpers only, no plugin object).
10
+ * 2. `@elizaos/plugin-openrouter` 2.0.0-beta.1 imports
11
+ * `buildCanonicalSystemPrompt` which the vendored core doesn't export.
12
+ * 3. A stale `bun install` re-resolves to a broken version.
13
+ *
14
+ * Adjust CHECKS below when you intentionally bump a plugin version.
15
+ */
16
+ import { existsSync, readFileSync } from "node:fs";
17
+ import path from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
22
+
23
+ const CHECKS = [
24
+ {
25
+ pkg: "@elizaos/plugin-openrouter",
26
+ entry: "dist/node/index.node.js",
27
+ minLines: 200,
28
+ mustContain: ["TEXT_LARGE", "TEXT_SMALL"],
29
+ mustNotContain: ["buildCanonicalSystemPrompt"],
30
+ pinned: "2.0.0-alpha.10",
31
+ why:
32
+ "alpha.11/12/13 ship a 79-line broken bundle; beta.1 imports the newer " +
33
+ "`buildCanonicalSystemPrompt` core API that vendored tokagent/packages/typescript " +
34
+ "does not export. alpha.10 is the last complete + compatible publish.",
35
+ },
36
+ ];
37
+
38
+ let hadFailure = false;
39
+ function fail(msg) {
40
+ console.error(`\n\x1b[31m[verify-llm-plugins]\x1b[0m ${msg}`);
41
+ hadFailure = true;
42
+ }
43
+ function pass(msg) {
44
+ console.log(`\x1b[32m[verify-llm-plugins]\x1b[0m ${msg}`);
45
+ }
46
+
47
+ for (const check of CHECKS) {
48
+ const pkgRoot = path.join(PROJECT_ROOT, "node_modules", check.pkg);
49
+ const entryPath = path.join(pkgRoot, check.entry);
50
+ const pkgJsonPath = path.join(pkgRoot, "package.json");
51
+
52
+ if (!existsSync(pkgRoot)) {
53
+ fail(`${check.pkg} is not installed. Run \`bun install\`. Expected pin: ${check.pinned}.`);
54
+ continue;
55
+ }
56
+
57
+ let installedVersion = "unknown";
58
+ try {
59
+ installedVersion = JSON.parse(readFileSync(pkgJsonPath, "utf8")).version;
60
+ } catch {}
61
+ if (installedVersion !== "unknown" && installedVersion !== check.pinned) {
62
+ fail(
63
+ `${check.pkg} is at ${installedVersion} but the project is pinned to ${check.pinned}.\n` +
64
+ ` Reason: ${check.why}\n` +
65
+ ` Fix: set package.json's overrides/resolutions for "${check.pkg}" to "${check.pinned}" and run \`bun install\` again.`,
66
+ );
67
+ continue;
68
+ }
69
+
70
+ if (!existsSync(entryPath)) {
71
+ fail(
72
+ `${check.pkg}@${installedVersion} is missing its entry file at ${check.entry}. ` +
73
+ `The published tarball is malformed. Re-pin to ${check.pinned}.`,
74
+ );
75
+ continue;
76
+ }
77
+
78
+ let body = "";
79
+ let lineCount = 0;
80
+ try {
81
+ body = readFileSync(entryPath, "utf8");
82
+ lineCount = body.split("\n").length;
83
+ } catch (err) {
84
+ fail(`Could not read ${entryPath}: ${err?.message ?? err}`);
85
+ continue;
86
+ }
87
+
88
+ if (lineCount < check.minLines) {
89
+ fail(
90
+ `${check.pkg}@${installedVersion} bundle at ${check.entry} has only ${lineCount} lines ` +
91
+ `(expected at least ${check.minLines}). This is the known "broken publish" failure mode. Pin to ${check.pinned}.`,
92
+ );
93
+ continue;
94
+ }
95
+
96
+ const missing = check.mustContain.filter((needle) => !body.includes(needle));
97
+ if (missing.length > 0) {
98
+ fail(
99
+ `${check.pkg}@${installedVersion} bundle is missing required tokens: ${missing.join(", ")}.\n` +
100
+ ` Reason: ${check.why}\n Fix: pin to ${check.pinned}.`,
101
+ );
102
+ continue;
103
+ }
104
+
105
+ const forbidden = check.mustNotContain.filter((needle) => body.includes(needle));
106
+ if (forbidden.length > 0) {
107
+ fail(
108
+ `${check.pkg}@${installedVersion} imports an API the vendored \`@elizaos/core\` does not expose: ${forbidden.join(", ")}.\n` +
109
+ ` Reason: ${check.why}\n Fix: pin to ${check.pinned}.`,
110
+ );
111
+ continue;
112
+ }
113
+
114
+ pass(`${check.pkg}@${installedVersion} ✓ (entry=${check.entry}, lines=${lineCount})`);
115
+ }
116
+
117
+ if (hadFailure) {
118
+ console.error(
119
+ "\n\x1b[31m[verify-llm-plugins]\x1b[0m One or more LLM-provider plugin checks failed. Chat will not work until this is resolved.\n",
120
+ );
121
+ process.exit(1);
122
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "generatedAt": "2026-05-19T21:32:47.891Z",
3
+ "generatedAt": "2026-05-25T17:16:25.028Z",
4
4
  "repoUrl": "https://github.com/elizaos/eliza",
5
5
  "templates": [
6
6
  {