@chat-js/cli 0.6.1 → 0.6.3
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/dist/index.js +17065 -16667
- package/package.json +1 -1
- package/templates/chat-app/app/(auth)/login/page.tsx +3 -3
- package/templates/chat-app/app/(chat)/api/chat/route.ts +4 -60
- package/templates/chat-app/app/not-found.tsx +2 -2
- package/templates/chat-app/chat.config.ts +3 -0
- package/templates/chat-app/components/ai-elements/actions.tsx +44 -44
- package/templates/chat-app/components/ai-elements/artifact.tsx +92 -92
- package/templates/chat-app/components/ai-elements/code-block.tsx +143 -143
- package/templates/chat-app/components/ai-elements/context.tsx +313 -313
- package/templates/chat-app/components/ai-elements/conversation.tsx +65 -65
- package/templates/chat-app/components/ai-elements/extra/conversation-content-scroll-area.tsx +29 -29
- package/templates/chat-app/components/ai-elements/extra/mcp-tool-header.tsx +27 -27
- package/templates/chat-app/components/ai-elements/message.tsx +341 -344
- package/templates/chat-app/components/ai-elements/parseIncompleteMarkdown.tsx +122 -122
- package/templates/chat-app/components/ai-elements/prompt-input.tsx +1059 -1059
- package/templates/chat-app/components/ai-elements/reasoning.tsx +131 -131
- package/templates/chat-app/components/ai-elements/response.tsx +15 -12
- package/templates/chat-app/components/ai-elements/sandbox.tsx +84 -84
- package/templates/chat-app/components/ai-elements/shimmer.tsx +47 -47
- package/templates/chat-app/components/ai-elements/suggestion.tsx +33 -33
- package/templates/chat-app/components/ai-elements/tool.tsx +118 -118
- package/templates/chat-app/components/app-sidebar-history-conditional.tsx +3 -3
- package/templates/chat-app/components/app-sidebar.tsx +3 -3
- package/templates/chat-app/components/connectors-dropdown.tsx +6 -3
- package/templates/chat-app/components/deep-research-progress.tsx +1 -1
- package/templates/chat-app/components/header-breadcrumb.tsx +14 -11
- package/templates/chat-app/components/internal-link.tsx +73 -0
- package/templates/chat-app/components/login-form.tsx +5 -5
- package/templates/chat-app/components/message-parts.tsx +1 -71
- package/templates/chat-app/components/model-selector.tsx +3 -3
- package/templates/chat-app/components/new-chat-button.tsx +4 -4
- package/templates/chat-app/components/part/document-common.tsx +3 -3
- package/templates/chat-app/components/part/document-tool.tsx +3 -3
- package/templates/chat-app/components/part/message-annotations.tsx +2 -2
- package/templates/chat-app/components/part/tool-part.tsx +92 -0
- package/templates/chat-app/components/project-chat-item.tsx +2 -2
- package/templates/chat-app/components/research-progress.tsx +2 -2
- package/templates/chat-app/components/research-task.tsx +1 -1
- package/templates/chat-app/components/research-tasks.tsx +1 -1
- package/templates/chat-app/components/settings/connectors-settings.tsx +4 -4
- package/templates/chat-app/components/settings/mcp-details-page.tsx +5 -5
- package/templates/chat-app/components/settings/settings-nav.tsx +3 -3
- package/templates/chat-app/components/sidebar-chat-item.tsx +4 -12
- package/templates/chat-app/components/sidebar-project-item.tsx +4 -11
- package/templates/chat-app/components/sidebar-top-row.tsx +7 -7
- package/templates/chat-app/components/sidebar-user-nav.tsx +3 -3
- package/templates/chat-app/components/signup-form.tsx +8 -5
- package/templates/chat-app/components/source-badge.tsx +3 -9
- package/templates/chat-app/components/sources.tsx +1 -1
- package/templates/chat-app/components/ui/accordion.tsx +32 -32
- package/templates/chat-app/components/ui/alert-dialog.tsx +103 -103
- package/templates/chat-app/components/ui/alert.tsx +36 -36
- package/templates/chat-app/components/ui/avatar.tsx +28 -28
- package/templates/chat-app/components/ui/badge.tsx +22 -22
- package/templates/chat-app/components/ui/breadcrumb.tsx +72 -72
- package/templates/chat-app/components/ui/button-group.tsx +58 -58
- package/templates/chat-app/components/ui/button.tsx +45 -45
- package/templates/chat-app/components/ui/card.tsx +65 -65
- package/templates/chat-app/components/ui/checkbox.tsx +16 -16
- package/templates/chat-app/components/ui/collapsible.tsx +1 -1
- package/templates/chat-app/components/ui/command.tsx +137 -137
- package/templates/chat-app/components/ui/dialog.tsx +94 -94
- package/templates/chat-app/components/ui/drawer.tsx +68 -68
- package/templates/chat-app/components/ui/dropdown-menu.tsx +184 -184
- package/templates/chat-app/components/ui/empty.tsx +76 -76
- package/templates/chat-app/components/ui/extra/action-container.tsx +3 -3
- package/templates/chat-app/components/ui/extra/scroll-area-viewport-ref.tsx +24 -24
- package/templates/chat-app/components/ui/form.tsx +112 -112
- package/templates/chat-app/components/ui/hover-card.tsx +25 -25
- package/templates/chat-app/components/ui/input-group.tsx +126 -126
- package/templates/chat-app/components/ui/input.tsx +13 -13
- package/templates/chat-app/components/ui/label.tsx +12 -12
- package/templates/chat-app/components/ui/popover.tsx +25 -25
- package/templates/chat-app/components/ui/progress.tsx +19 -19
- package/templates/chat-app/components/ui/resizable.tsx +27 -27
- package/templates/chat-app/components/ui/scroll-area.tsx +30 -30
- package/templates/chat-app/components/ui/select.tsx +108 -108
- package/templates/chat-app/components/ui/separator.tsx +16 -16
- package/templates/chat-app/components/ui/sheet.tsx +91 -91
- package/templates/chat-app/components/ui/sidebar.tsx +615 -615
- package/templates/chat-app/components/ui/skeleton.tsx +7 -7
- package/templates/chat-app/components/ui/slider.tsx +50 -50
- package/templates/chat-app/components/ui/spinner.tsx +8 -8
- package/templates/chat-app/components/ui/switch.tsx +16 -16
- package/templates/chat-app/components/ui/table.tsx +71 -71
- package/templates/chat-app/components/ui/tabs.tsx +31 -31
- package/templates/chat-app/components/ui/textarea.tsx +10 -10
- package/templates/chat-app/components/ui/toggle.tsx +31 -31
- package/templates/chat-app/components/ui/tooltip.tsx +48 -48
- package/templates/chat-app/components/upgrade-cta/limit-display.tsx +7 -7
- package/templates/chat-app/components/upgrade-cta/login-cta-banner.tsx +3 -3
- package/templates/chat-app/components/upgrade-cta/login-prompt.tsx +3 -3
- package/templates/chat-app/hooks/use-mobile.ts +13 -13
- package/templates/chat-app/lib/ai/core-chat-agent.ts +25 -14
- package/templates/chat-app/lib/ai/eval-agent.ts +4 -5
- package/templates/chat-app/lib/ai/gateway-model-defaults.ts +24 -0
- package/templates/chat-app/lib/ai/installed-tools.ts +12 -0
- package/templates/chat-app/lib/ai/mcp/mcp-client.ts +2 -2
- package/templates/chat-app/lib/ai/models.generated.ts +4236 -4585
- package/templates/chat-app/lib/ai/tool-renderer-registry.ts +31 -0
- package/templates/chat-app/lib/ai/types.ts +15 -20
- package/templates/chat-app/lib/config-requirements.ts +11 -6
- package/templates/chat-app/lib/config-schema.ts +24 -0
- package/templates/chat-app/lib/stores/hooks-message-parts.ts +1 -1
- package/templates/chat-app/lib/utils.ts +157 -157
- package/templates/chat-app/package.json +1 -1
- package/templates/chat-app/scripts/check-env.ts +229 -2
- package/templates/chat-app/tools/chatjs/_shared/lib/tool-part.ts +5 -0
- package/templates/chat-app/{components/part/weather.tsx → tools/chatjs/get-weather/renderer.tsx} +24 -38
- package/templates/chat-app/{components/part/retrieve-url.tsx → tools/chatjs/retrieve-url/renderer.tsx} +20 -15
- package/templates/chat-app/{lib/ai/tools/retrieve-url.ts → tools/chatjs/retrieve-url/tool.ts} +46 -7
- package/templates/chat-app/tools/chatjs/tools.ts +16 -0
- package/templates/chat-app/tools/chatjs/ui.ts +17 -0
- package/templates/chat-app/tools/chatjs/word-count/renderer.tsx +50 -0
- package/templates/chat-app/tools/chatjs/word-count/tool.ts +30 -0
- package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.ts +3 -5
- package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/deep-research.ts +2 -3
- package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/pipeline.ts +1 -1
- package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/types.ts +1 -1
- package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/utils.ts +7 -7
- package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/types.ts +1 -1
- package/templates/chat-app/{lib/ai/tools → tools/platform}/generate-video.ts +4 -6
- package/templates/chat-app/{lib/ai/tools → tools/platform}/read-document.ts +2 -2
- package/templates/chat-app/{lib/ai/tools → tools/platform}/steps/multi-query-web-search.ts +1 -1
- package/templates/chat-app/{lib/ai/tools → tools/platform}/steps/web-search.ts +1 -1
- package/templates/chat-app/{lib/ai/tools → tools/platform}/tools.ts +54 -30
- package/templates/chat-app/{lib/ai/tools → tools/platform}/web-search.ts +7 -5
- package/templates/electron/CHANGELOG.md +16 -2
- package/templates/electron/package.json +1 -1
- package/templates/chat-app/lib/ai/tools/tools-definitions.ts +0 -83
- /package/templates/chat-app/{lib/ai/tools/get-weather.ts → tools/chatjs/get-weather/tool.ts} +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.javascript.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.python.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.shared.test.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.shared.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.types.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/configuration.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/prompts.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/researcher-agent.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/supervisor-agent.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/code-guidelines.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/create-code-document.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/create-sheet-document.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/create-text-document.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/edit-code-document.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/edit-sheet-document.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/edit-text-document.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/sheet-guidelines.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/text-guidelines.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/generate-image.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/research-updates-schema.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/steps/search-utils.ts +0 -0
- /package/templates/chat-app/{lib/ai/tools → tools/platform}/types.ts +0 -0
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* Run via `bun run check-env` or automatically in prebuild.
|
|
6
6
|
*/
|
|
7
7
|
import "dotenv/config";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
// biome-ignore lint/performance/noNamespaceImport: TypeScript API requires namespace import due to extensive usage
|
|
12
|
+
import * as ts from "typescript";
|
|
8
13
|
import type { GatewayType } from "../lib/ai/gateways/registry";
|
|
9
14
|
import { generatedForGateway } from "../lib/ai/models.generated";
|
|
10
15
|
import { config } from "../lib/config";
|
|
@@ -23,6 +28,180 @@ interface ValidationError {
|
|
|
23
28
|
missing: string[];
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
type StaticToolEnvVar = {
|
|
32
|
+
description?: string;
|
|
33
|
+
options: string[][];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type StaticToolEnvVars = StaticToolEnvVar[];
|
|
37
|
+
|
|
38
|
+
type StaticToolMetadata = {
|
|
39
|
+
toolEnvVars: StaticToolEnvVars;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const projectRoot = path.resolve(
|
|
43
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
44
|
+
".."
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function unwrapExpression(node: ts.Expression): ts.Expression {
|
|
48
|
+
if (ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
49
|
+
return unwrapExpression(node.expression);
|
|
50
|
+
}
|
|
51
|
+
return node;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readString(node: ts.Expression): string | null {
|
|
55
|
+
const expr = unwrapExpression(node);
|
|
56
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
57
|
+
return expr.text;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readStringArray(node: ts.Expression): string[] | null {
|
|
63
|
+
const expr = unwrapExpression(node);
|
|
64
|
+
if (!ts.isArrayLiteralExpression(expr)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const values: string[] = [];
|
|
69
|
+
for (const element of expr.elements) {
|
|
70
|
+
if (!ts.isExpression(element)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const value = readString(element);
|
|
74
|
+
if (value === null) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
values.push(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return values;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: AST traversal logic is inherently complex
|
|
84
|
+
function readToolEnvVar(node: ts.Expression): StaticToolEnvVar | null {
|
|
85
|
+
const expr = unwrapExpression(node);
|
|
86
|
+
if (!ts.isObjectLiteralExpression(expr)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let description: string | null = null;
|
|
91
|
+
let options: string[][] | null = null;
|
|
92
|
+
|
|
93
|
+
for (const property of expr.properties) {
|
|
94
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nameNode = property.name;
|
|
99
|
+
const name =
|
|
100
|
+
ts.isIdentifier(nameNode) || ts.isStringLiteral(nameNode)
|
|
101
|
+
? nameNode.text
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
if (name === "description") {
|
|
105
|
+
description = readString(property.initializer);
|
|
106
|
+
} else if (name === "options") {
|
|
107
|
+
const outer = unwrapExpression(property.initializer);
|
|
108
|
+
if (!ts.isArrayLiteralExpression(outer)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const groups: string[][] = [];
|
|
113
|
+
for (const element of outer.elements) {
|
|
114
|
+
if (!ts.isExpression(element)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const group = readStringArray(element);
|
|
118
|
+
if (group === null) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
groups.push(group);
|
|
122
|
+
}
|
|
123
|
+
options = groups;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return options ? { ...(description ? { description } : {}), options } : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readToolEnvVars(node: ts.Expression): StaticToolEnvVars {
|
|
131
|
+
const expr = unwrapExpression(node);
|
|
132
|
+
if (!ts.isArrayLiteralExpression(expr)) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const toolEnvVars: StaticToolEnvVars = [];
|
|
137
|
+
for (const element of expr.elements) {
|
|
138
|
+
if (!ts.isExpression(element)) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const toolEnvVar = readToolEnvVar(element);
|
|
142
|
+
if (!toolEnvVar) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
toolEnvVars.push(toolEnvVar);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return toolEnvVars;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readStaticToolMetadata(sourceText: string): StaticToolMetadata {
|
|
152
|
+
const sourceFile = ts.createSourceFile(
|
|
153
|
+
"tool.ts",
|
|
154
|
+
sourceText,
|
|
155
|
+
ts.ScriptTarget.ESNext,
|
|
156
|
+
true,
|
|
157
|
+
ts.ScriptKind.TS
|
|
158
|
+
);
|
|
159
|
+
const toolEnvVars: StaticToolEnvVars = [];
|
|
160
|
+
|
|
161
|
+
function visit(node: ts.Node): void {
|
|
162
|
+
if (
|
|
163
|
+
ts.isVariableStatement(node) &&
|
|
164
|
+
node.modifiers?.some(
|
|
165
|
+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword
|
|
166
|
+
)
|
|
167
|
+
) {
|
|
168
|
+
for (const declaration of node.declarationList.declarations) {
|
|
169
|
+
if (declaration.name.getText() !== "toolEnvVars") {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const initializer = declaration.initializer;
|
|
173
|
+
if (initializer && ts.isExpression(initializer)) {
|
|
174
|
+
toolEnvVars.push(...readToolEnvVars(initializer));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
ts.forEachChild(node, visit);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
visit(sourceFile);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
toolEnvVars,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveToolsDir(toolsPath: string): string {
|
|
190
|
+
if (toolsPath.startsWith("@/")) {
|
|
191
|
+
return path.resolve(projectRoot, toolsPath.slice(2));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (toolsPath.startsWith("./") || toolsPath.startsWith("../")) {
|
|
195
|
+
return path.resolve(projectRoot, toolsPath);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (path.isAbsolute(toolsPath)) {
|
|
199
|
+
return toolsPath;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return path.resolve(projectRoot, toolsPath);
|
|
203
|
+
}
|
|
204
|
+
|
|
26
205
|
function validateGatewayKey(env: NodeJS.ProcessEnv): ValidationError | null {
|
|
27
206
|
// Prevent TS from narrowing to the current literal config value.
|
|
28
207
|
const gateway = (() => config.ai.gateway as GatewayType)();
|
|
@@ -131,6 +310,52 @@ function validateAuthentication(env: NodeJS.ProcessEnv): ValidationError[] {
|
|
|
131
310
|
return errors;
|
|
132
311
|
}
|
|
133
312
|
|
|
313
|
+
async function validateInstalledTools(
|
|
314
|
+
env: NodeJS.ProcessEnv
|
|
315
|
+
): Promise<ValidationError[]> {
|
|
316
|
+
const toolsDir = resolveToolsDir(config.paths.tools);
|
|
317
|
+
const entries = await fs
|
|
318
|
+
.readdir(toolsDir, { withFileTypes: true })
|
|
319
|
+
.catch((error: NodeJS.ErrnoException) => {
|
|
320
|
+
if (error.code === "ENOENT") {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
});
|
|
325
|
+
const errors: ValidationError[] = [];
|
|
326
|
+
|
|
327
|
+
for (const entry of entries) {
|
|
328
|
+
if (!entry.isDirectory() || entry.name.startsWith("_")) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const toolPath = path.join(toolsDir, entry.name, "tool.ts");
|
|
333
|
+
const exists = await fs
|
|
334
|
+
.access(toolPath)
|
|
335
|
+
.then(() => true)
|
|
336
|
+
.catch(() => false);
|
|
337
|
+
|
|
338
|
+
if (!exists) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const toolSource = await fs.readFile(toolPath, "utf8");
|
|
343
|
+
const mod = readStaticToolMetadata(toolSource);
|
|
344
|
+
|
|
345
|
+
for (const toolEnvVar of mod.toolEnvVars) {
|
|
346
|
+
const missing = getMissingRequirement(toolEnvVar, env);
|
|
347
|
+
if (missing) {
|
|
348
|
+
errors.push({
|
|
349
|
+
feature: `tools.${entry.name}`,
|
|
350
|
+
missing: [missing],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return errors;
|
|
357
|
+
}
|
|
358
|
+
|
|
134
359
|
function validateBaseUrl(env: NodeJS.ProcessEnv): ValidationError | null {
|
|
135
360
|
const isProduction = env.NODE_ENV === "production" || env.VERCEL === "1";
|
|
136
361
|
if (!isProduction) {
|
|
@@ -157,7 +382,7 @@ function checkGatewaySnapshot(): string | null {
|
|
|
157
382
|
return `models.generated.ts was built for "${generatedForGateway}" but config uses "${config.ai.gateway}". Run \`bun fetch:models\` to update the fallback snapshot.`;
|
|
158
383
|
}
|
|
159
384
|
|
|
160
|
-
function checkEnv(): void {
|
|
385
|
+
async function checkEnv(): Promise<void> {
|
|
161
386
|
const env = process.env;
|
|
162
387
|
if (isPlaywrightTestEnvironment(env)) {
|
|
163
388
|
console.log(
|
|
@@ -169,11 +394,13 @@ function checkEnv(): void {
|
|
|
169
394
|
}
|
|
170
395
|
|
|
171
396
|
const baseUrlError = validateBaseUrl(env);
|
|
397
|
+
const installedToolErrors = await validateInstalledTools(env);
|
|
172
398
|
const errors = [
|
|
173
399
|
...(baseUrlError ? [baseUrlError] : []),
|
|
174
400
|
...validateFeatures(env),
|
|
175
401
|
...validateAiTools(env),
|
|
176
402
|
...validateAuthentication(env),
|
|
403
|
+
...installedToolErrors,
|
|
177
404
|
];
|
|
178
405
|
|
|
179
406
|
if (errors.length > 0) {
|
|
@@ -195,4 +422,4 @@ function checkEnv(): void {
|
|
|
195
422
|
console.log("✅ Environment validation passed");
|
|
196
423
|
}
|
|
197
424
|
|
|
198
|
-
checkEnv();
|
|
425
|
+
await checkEnv();
|
package/templates/chat-app/{components/part/weather.tsx → tools/chatjs/get-weather/renderer.tsx}
RENAMED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { format, isWithinInterval } from "date-fns";
|
|
4
4
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
5
|
-
import type { WeatherAtLocation } from "@/lib/ai/tools/get-weather";
|
|
6
|
-
import type { ChatMessage } from "@/lib/ai/types";
|
|
7
5
|
import { cn } from "@/lib/utils";
|
|
6
|
+
import type { TypelessToolPartFromTool } from "@/tools/chatjs/_shared/lib/tool-part";
|
|
7
|
+
import type { getWeather, WeatherAtLocation } from "./tool";
|
|
8
|
+
|
|
9
|
+
type GetWeatherRendererTool = TypelessToolPartFromTool<typeof getWeather>;
|
|
8
10
|
|
|
9
11
|
const SAMPLE = {
|
|
10
12
|
latitude: 37.763_283,
|
|
@@ -165,11 +167,6 @@ function n(num: number): number {
|
|
|
165
167
|
return Math.ceil(num);
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
export type WeatherTool = Extract<
|
|
169
|
-
ChatMessage["parts"][number],
|
|
170
|
-
{ type: "tool-getWeather" }
|
|
171
|
-
>;
|
|
172
|
-
|
|
173
170
|
function WeatherCard({
|
|
174
171
|
weatherAtLocation,
|
|
175
172
|
}: {
|
|
@@ -188,15 +185,10 @@ function WeatherCard({
|
|
|
188
185
|
});
|
|
189
186
|
|
|
190
187
|
const isMobile = useIsMobile();
|
|
191
|
-
|
|
192
188
|
const hoursToShow = isMobile ? 5 : 6;
|
|
193
|
-
|
|
194
|
-
// Find the index of the current time or the next closest time
|
|
195
189
|
const currentTimeIndex = weatherAtLocation.hourly.time.findIndex(
|
|
196
190
|
(time) => new Date(time) >= new Date(weatherAtLocation.current.time)
|
|
197
191
|
);
|
|
198
|
-
|
|
199
|
-
// Slice the arrays to get the desired number of items
|
|
200
192
|
const displayTimes = weatherAtLocation.hourly.time.slice(
|
|
201
193
|
currentTimeIndex,
|
|
202
194
|
currentTimeIndex + hoursToShow
|
|
@@ -210,12 +202,8 @@ function WeatherCard({
|
|
|
210
202
|
<div
|
|
211
203
|
className={cn(
|
|
212
204
|
"skeleton-bg flex max-w-[500px] flex-col gap-4 rounded-2xl p-4",
|
|
213
|
-
{
|
|
214
|
-
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
"bg-indigo-900": !isDay,
|
|
218
|
-
}
|
|
205
|
+
{ "bg-blue-400": isDay },
|
|
206
|
+
{ "bg-indigo-900": !isDay }
|
|
219
207
|
)}
|
|
220
208
|
>
|
|
221
209
|
<div className="flex flex-row items-center justify-between">
|
|
@@ -223,12 +211,8 @@ function WeatherCard({
|
|
|
223
211
|
<div
|
|
224
212
|
className={cn(
|
|
225
213
|
"skeleton-div size-10 rounded-full",
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
"bg-indigo-100": !isDay,
|
|
231
|
-
}
|
|
214
|
+
{ "bg-yellow-300": isDay },
|
|
215
|
+
{ "bg-indigo-100": !isDay }
|
|
232
216
|
)}
|
|
233
217
|
/>
|
|
234
218
|
<div className="font-medium text-4xl text-blue-50">
|
|
@@ -249,12 +233,8 @@ function WeatherCard({
|
|
|
249
233
|
<div
|
|
250
234
|
className={cn(
|
|
251
235
|
"skeleton-div size-6 rounded-full",
|
|
252
|
-
{
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
"bg-indigo-200": !isDay,
|
|
257
|
-
}
|
|
236
|
+
{ "bg-yellow-300": isDay },
|
|
237
|
+
{ "bg-indigo-200": !isDay }
|
|
258
238
|
)}
|
|
259
239
|
/>
|
|
260
240
|
<div className="text-blue-50 text-sm">
|
|
@@ -268,18 +248,24 @@ function WeatherCard({
|
|
|
268
248
|
);
|
|
269
249
|
}
|
|
270
250
|
|
|
271
|
-
export function
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
251
|
+
export function GetWeatherRenderer({
|
|
252
|
+
tool,
|
|
253
|
+
}: {
|
|
254
|
+
tool: GetWeatherRendererTool;
|
|
255
|
+
messageId: string;
|
|
256
|
+
isReadonly: boolean;
|
|
257
|
+
}) {
|
|
258
|
+
if (tool.state !== "output-available") {
|
|
277
259
|
return (
|
|
278
260
|
<div className="skeleton" key={tool.toolCallId}>
|
|
279
|
-
<WeatherCard weatherAtLocation={
|
|
261
|
+
<WeatherCard weatherAtLocation={SAMPLE} />
|
|
280
262
|
</div>
|
|
281
263
|
);
|
|
282
264
|
}
|
|
283
265
|
|
|
284
|
-
|
|
266
|
+
if (!tool.output) {
|
|
267
|
+
return <WeatherCard weatherAtLocation={SAMPLE} />;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return <WeatherCard weatherAtLocation={tool.output} />;
|
|
285
271
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { ChevronDown, ExternalLink, Globe, TextIcon } from "lucide-react";
|
|
4
|
-
import Image from "next/image";
|
|
5
4
|
import ReactMarkdown from "react-markdown";
|
|
6
|
-
import type {
|
|
5
|
+
import type { TypelessToolPartFromTool } from "@/tools/chatjs/_shared/lib/tool-part";
|
|
6
|
+
import type { retrieveUrl } from "./tool";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
ChatMessage["parts"][number],
|
|
10
|
-
{ type: "tool-retrieveUrl" }
|
|
11
|
-
>;
|
|
8
|
+
type RetrieveUrlRendererTool = TypelessToolPartFromTool<typeof retrieveUrl>;
|
|
12
9
|
|
|
13
10
|
function LoadingState() {
|
|
14
11
|
return (
|
|
@@ -77,13 +74,7 @@ function RetrievedContentHeader({ firstItem }: { firstItem: unknown }) {
|
|
|
77
74
|
<div className="flex items-start gap-4">
|
|
78
75
|
<div className="relative h-10 w-10 shrink-0">
|
|
79
76
|
<div className="absolute inset-0 rounded-lg bg-linear-to-br from-primary/10 to-transparent" />
|
|
80
|
-
<
|
|
81
|
-
alt=""
|
|
82
|
-
className="absolute inset-0 m-auto"
|
|
83
|
-
height={20}
|
|
84
|
-
src={`https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(url)}`}
|
|
85
|
-
width={20}
|
|
86
|
-
/>
|
|
77
|
+
<Globe className="absolute inset-0 m-auto h-5 w-5 text-primary/70" />
|
|
87
78
|
</div>
|
|
88
79
|
<div className="min-w-0 flex-1 space-y-2">
|
|
89
80
|
<h2 className="truncate font-semibold text-foreground text-lg tracking-tight">
|
|
@@ -159,11 +150,25 @@ function getErrorMessage(result: unknown, firstItem: unknown): string | null {
|
|
|
159
150
|
return topLevelError ?? firstItemError ?? null;
|
|
160
151
|
}
|
|
161
152
|
|
|
162
|
-
export function
|
|
163
|
-
|
|
153
|
+
export function RetrieveUrlRenderer({
|
|
154
|
+
tool,
|
|
155
|
+
}: {
|
|
156
|
+
tool: RetrieveUrlRendererTool;
|
|
157
|
+
messageId: string;
|
|
158
|
+
isReadonly: boolean;
|
|
159
|
+
}) {
|
|
160
|
+
if (tool.state === "input-available" || tool.state === "input-streaming") {
|
|
164
161
|
return <LoadingState />;
|
|
165
162
|
}
|
|
166
163
|
|
|
164
|
+
if (tool.state !== "output-available") {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!tool.output) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
167
172
|
const { output: result } = tool;
|
|
168
173
|
const firstItem = getFirstItem(result);
|
|
169
174
|
const errorMessage = getErrorMessage(result, firstItem);
|
package/templates/chat-app/{lib/ai/tools/retrieve-url.ts → tools/chatjs/retrieve-url/tool.ts}
RENAMED
|
@@ -2,7 +2,19 @@ import FirecrawlApp from "@mendable/firecrawl-js";
|
|
|
2
2
|
import { tool } from "ai";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { env } from "@/lib/env";
|
|
5
|
-
import { createModuleLogger } from "
|
|
5
|
+
import { createModuleLogger } from "@/lib/logger";
|
|
6
|
+
|
|
7
|
+
type ToolEnvVars = {
|
|
8
|
+
description?: string;
|
|
9
|
+
options: string[][];
|
|
10
|
+
}[];
|
|
11
|
+
|
|
12
|
+
export const toolEnvVars: ToolEnvVars = [
|
|
13
|
+
{
|
|
14
|
+
description: "FIRECRAWL_API_KEY",
|
|
15
|
+
options: [["FIRECRAWL_API_KEY"]],
|
|
16
|
+
},
|
|
17
|
+
];
|
|
6
18
|
|
|
7
19
|
const log = createModuleLogger("tools/retrieve-url");
|
|
8
20
|
|
|
@@ -10,6 +22,22 @@ const app = env.FIRECRAWL_API_KEY
|
|
|
10
22
|
? new FirecrawlApp({ apiKey: env.FIRECRAWL_API_KEY })
|
|
11
23
|
: null;
|
|
12
24
|
|
|
25
|
+
function parseUrl(url: string): URL | null {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function redactUrl(url: URL): string {
|
|
38
|
+
return `${url.origin}${url.pathname}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
13
41
|
export const retrieveUrl = tool({
|
|
14
42
|
description: `Fetch structured information from a single URL via Firecrawl.
|
|
15
43
|
|
|
@@ -29,7 +57,16 @@ Avoid:
|
|
|
29
57
|
"Firecrawl is not configured. Set FIRECRAWL_API_KEY to enable retrieval.",
|
|
30
58
|
};
|
|
31
59
|
}
|
|
32
|
-
const
|
|
60
|
+
const parsedUrl = parseUrl(url);
|
|
61
|
+
if (!parsedUrl) {
|
|
62
|
+
return {
|
|
63
|
+
error: "Please provide a valid http:// or https:// URL.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const redactedUrl = redactUrl(parsedUrl);
|
|
68
|
+
const normalizedUrl = parsedUrl.toString();
|
|
69
|
+
const content = await app.scrapeUrl(normalizedUrl);
|
|
33
70
|
if (!(content.success && content.metadata)) {
|
|
34
71
|
return {
|
|
35
72
|
results: [
|
|
@@ -40,7 +77,6 @@ Avoid:
|
|
|
40
77
|
};
|
|
41
78
|
}
|
|
42
79
|
|
|
43
|
-
// Define schema for extracting missing content
|
|
44
80
|
const schema = z.object({
|
|
45
81
|
title: z.string(),
|
|
46
82
|
content: z.string(),
|
|
@@ -51,9 +87,8 @@ Avoid:
|
|
|
51
87
|
let description = content.metadata.description;
|
|
52
88
|
let extractedContent = content.markdown;
|
|
53
89
|
|
|
54
|
-
// If any content is missing, use extract to get it
|
|
55
90
|
if (!(title && description && extractedContent)) {
|
|
56
|
-
const extractResult = await app.extract([
|
|
91
|
+
const extractResult = await app.extract([normalizedUrl], {
|
|
57
92
|
prompt:
|
|
58
93
|
"Extract the page title, main content, and a brief description.",
|
|
59
94
|
schema,
|
|
@@ -71,14 +106,18 @@ Avoid:
|
|
|
71
106
|
{
|
|
72
107
|
title: title || "Untitled",
|
|
73
108
|
content: extractedContent || "",
|
|
74
|
-
url:
|
|
109
|
+
url: redactedUrl,
|
|
75
110
|
description: description || "",
|
|
76
111
|
language: content.metadata.language,
|
|
77
112
|
},
|
|
78
113
|
],
|
|
79
114
|
};
|
|
80
115
|
} catch (error) {
|
|
81
|
-
|
|
116
|
+
const parsedUrl = parseUrl(url);
|
|
117
|
+
log.error(
|
|
118
|
+
{ err: error, url: parsedUrl ? redactUrl(parsedUrl) : "<invalid-url>" },
|
|
119
|
+
"Firecrawl API error in retrieveUrl tool"
|
|
120
|
+
);
|
|
82
121
|
return { error: "Failed to retrieve content" };
|
|
83
122
|
}
|
|
84
123
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Server-side tools installed via `chatjs add`.
|
|
2
|
+
// This file is fully managed by the CLI — do not edit manually.
|
|
3
|
+
|
|
4
|
+
// [chatjs-registry:tool-imports]
|
|
5
|
+
import { getWeather } from "@/tools/chatjs/get-weather/tool";
|
|
6
|
+
import { retrieveUrl } from "@/tools/chatjs/retrieve-url/tool";
|
|
7
|
+
import { wordCount } from "@/tools/chatjs/word-count/tool";
|
|
8
|
+
// [/chatjs-registry:tool-imports]
|
|
9
|
+
|
|
10
|
+
export const tools = {
|
|
11
|
+
// [chatjs-registry:tools]
|
|
12
|
+
getWeather,
|
|
13
|
+
retrieveUrl,
|
|
14
|
+
wordCount,
|
|
15
|
+
// [/chatjs-registry:tools]
|
|
16
|
+
} as const;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Client-side tool renderers installed via `chatjs add`.
|
|
2
|
+
// This file is fully managed by the CLI — do not edit manually.
|
|
3
|
+
import type { ToolRendererRegistry } from "@/lib/ai/tool-renderer-registry";
|
|
4
|
+
|
|
5
|
+
// [chatjs-registry:ui-imports]
|
|
6
|
+
import { GetWeatherRenderer } from "@/tools/chatjs/get-weather/renderer";
|
|
7
|
+
import { RetrieveUrlRenderer } from "@/tools/chatjs/retrieve-url/renderer";
|
|
8
|
+
import { WordCountRenderer } from "@/tools/chatjs/word-count/renderer";
|
|
9
|
+
// [/chatjs-registry:ui-imports]
|
|
10
|
+
|
|
11
|
+
export const ui = {
|
|
12
|
+
// [chatjs-registry:ui]
|
|
13
|
+
"tool-getWeather": GetWeatherRenderer,
|
|
14
|
+
"tool-retrieveUrl": RetrieveUrlRenderer,
|
|
15
|
+
"tool-wordCount": WordCountRenderer,
|
|
16
|
+
// [/chatjs-registry:ui]
|
|
17
|
+
} satisfies ToolRendererRegistry;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { TypelessToolPartFromTool } from "@/tools/chatjs/_shared/lib/tool-part";
|
|
4
|
+
import type { wordCount } from "./tool";
|
|
5
|
+
|
|
6
|
+
type WordCountRendererTool = TypelessToolPartFromTool<typeof wordCount>;
|
|
7
|
+
|
|
8
|
+
export function WordCountRenderer({
|
|
9
|
+
tool,
|
|
10
|
+
}: {
|
|
11
|
+
tool: WordCountRendererTool;
|
|
12
|
+
messageId: string;
|
|
13
|
+
isReadonly: boolean;
|
|
14
|
+
}) {
|
|
15
|
+
if (tool.state === "input-available") {
|
|
16
|
+
return (
|
|
17
|
+
<div className="rounded-lg border p-3 text-muted-foreground text-sm">
|
|
18
|
+
Counting words...
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (tool.state !== "output-available") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!tool.output) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { words, characters, charactersNoSpaces, sentences } = tool.output;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="grid grid-cols-2 gap-2 rounded-lg border p-3 text-sm sm:grid-cols-4">
|
|
35
|
+
<Stat label="Words" value={words} />
|
|
36
|
+
<Stat label="Characters" value={characters} />
|
|
37
|
+
<Stat label="No spaces" value={charactersNoSpaces} />
|
|
38
|
+
<Stat label="Sentences" value={sentences} />
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Stat({ label, value }: { label: string; value: number }) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex flex-col items-center gap-1">
|
|
46
|
+
<span className="font-semibold text-lg">{value}</span>
|
|
47
|
+
<span className="text-muted-foreground text-xs">{label}</span>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const WORD_SPLIT_REGEX = /\s+/;
|
|
5
|
+
const SENTENCE_SPLIT_REGEX = /[.!?]+/;
|
|
6
|
+
|
|
7
|
+
export const wordCount = tool({
|
|
8
|
+
description: "Count the words, characters, and sentences in a given text",
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
text: z.string().describe("The text to analyze"),
|
|
11
|
+
}),
|
|
12
|
+
execute: ({ text }: { text: string }) => {
|
|
13
|
+
const words =
|
|
14
|
+
text.trim() === "" ? 0 : text.trim().split(WORD_SPLIT_REGEX).length;
|
|
15
|
+
const characters = text.length;
|
|
16
|
+
const charactersNoSpaces = text.replace(/\s/g, "").length;
|
|
17
|
+
const sentences = text
|
|
18
|
+
.split(SENTENCE_SPLIT_REGEX)
|
|
19
|
+
.filter((s) => s.trim().length > 0).length;
|
|
20
|
+
|
|
21
|
+
return { words, characters, charactersNoSpaces, sentences };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type WordCountOutput = {
|
|
26
|
+
words: number;
|
|
27
|
+
characters: number;
|
|
28
|
+
charactersNoSpaces: number;
|
|
29
|
+
sentences: number;
|
|
30
|
+
};
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
type SupportedExecutionLanguage,
|
|
16
16
|
supportedExecutionLanguages,
|
|
17
17
|
} from "./code-execution.types";
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
const COST_CENTS = 5; // Vercel Sandbox execution
|
|
19
20
|
|
|
20
21
|
const languageSchema = z.enum(supportedExecutionLanguages);
|
|
21
22
|
|
|
@@ -115,10 +116,7 @@ Output rules:
|
|
|
115
116
|
requestId,
|
|
116
117
|
});
|
|
117
118
|
|
|
118
|
-
costAccumulator?.addAPICost(
|
|
119
|
-
"codeExecution",
|
|
120
|
-
toolsDefinitions.codeExecution.cost
|
|
121
|
-
);
|
|
119
|
+
costAccumulator?.addAPICost("codeExecution", COST_CENTS);
|
|
122
120
|
|
|
123
121
|
return result;
|
|
124
122
|
} catch (err) {
|