@chat-js/cli 0.6.1 → 0.6.2

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 (153) hide show
  1. package/dist/index.js +16938 -16786
  2. package/package.json +1 -1
  3. package/templates/chat-app/app/(auth)/login/page.tsx +3 -3
  4. package/templates/chat-app/app/(chat)/api/chat/route.ts +4 -60
  5. package/templates/chat-app/app/not-found.tsx +2 -2
  6. package/templates/chat-app/chat.config.ts +3 -0
  7. package/templates/chat-app/components/ai-elements/actions.tsx +44 -44
  8. package/templates/chat-app/components/ai-elements/artifact.tsx +92 -92
  9. package/templates/chat-app/components/ai-elements/code-block.tsx +143 -143
  10. package/templates/chat-app/components/ai-elements/context.tsx +313 -313
  11. package/templates/chat-app/components/ai-elements/conversation.tsx +65 -65
  12. package/templates/chat-app/components/ai-elements/extra/conversation-content-scroll-area.tsx +29 -29
  13. package/templates/chat-app/components/ai-elements/extra/mcp-tool-header.tsx +27 -27
  14. package/templates/chat-app/components/ai-elements/message.tsx +341 -344
  15. package/templates/chat-app/components/ai-elements/parseIncompleteMarkdown.tsx +122 -122
  16. package/templates/chat-app/components/ai-elements/prompt-input.tsx +1059 -1059
  17. package/templates/chat-app/components/ai-elements/reasoning.tsx +131 -131
  18. package/templates/chat-app/components/ai-elements/response.tsx +15 -12
  19. package/templates/chat-app/components/ai-elements/sandbox.tsx +84 -84
  20. package/templates/chat-app/components/ai-elements/shimmer.tsx +47 -47
  21. package/templates/chat-app/components/ai-elements/suggestion.tsx +33 -33
  22. package/templates/chat-app/components/ai-elements/tool.tsx +118 -118
  23. package/templates/chat-app/components/app-sidebar-history-conditional.tsx +3 -3
  24. package/templates/chat-app/components/app-sidebar.tsx +3 -3
  25. package/templates/chat-app/components/connectors-dropdown.tsx +6 -3
  26. package/templates/chat-app/components/deep-research-progress.tsx +1 -1
  27. package/templates/chat-app/components/header-breadcrumb.tsx +14 -11
  28. package/templates/chat-app/components/internal-link.tsx +73 -0
  29. package/templates/chat-app/components/login-form.tsx +5 -5
  30. package/templates/chat-app/components/message-parts.tsx +1 -71
  31. package/templates/chat-app/components/model-selector.tsx +3 -3
  32. package/templates/chat-app/components/new-chat-button.tsx +4 -4
  33. package/templates/chat-app/components/part/document-common.tsx +3 -3
  34. package/templates/chat-app/components/part/document-tool.tsx +3 -3
  35. package/templates/chat-app/components/part/message-annotations.tsx +2 -2
  36. package/templates/chat-app/components/part/tool-part.tsx +92 -0
  37. package/templates/chat-app/components/project-chat-item.tsx +2 -2
  38. package/templates/chat-app/components/research-progress.tsx +2 -2
  39. package/templates/chat-app/components/research-task.tsx +1 -1
  40. package/templates/chat-app/components/research-tasks.tsx +1 -1
  41. package/templates/chat-app/components/settings/connectors-settings.tsx +4 -4
  42. package/templates/chat-app/components/settings/mcp-details-page.tsx +5 -5
  43. package/templates/chat-app/components/settings/settings-nav.tsx +3 -3
  44. package/templates/chat-app/components/sidebar-chat-item.tsx +4 -12
  45. package/templates/chat-app/components/sidebar-project-item.tsx +4 -11
  46. package/templates/chat-app/components/sidebar-top-row.tsx +7 -7
  47. package/templates/chat-app/components/sidebar-user-nav.tsx +3 -3
  48. package/templates/chat-app/components/signup-form.tsx +8 -5
  49. package/templates/chat-app/components/source-badge.tsx +3 -9
  50. package/templates/chat-app/components/sources.tsx +1 -1
  51. package/templates/chat-app/components/ui/accordion.tsx +32 -32
  52. package/templates/chat-app/components/ui/alert-dialog.tsx +103 -103
  53. package/templates/chat-app/components/ui/alert.tsx +36 -36
  54. package/templates/chat-app/components/ui/avatar.tsx +28 -28
  55. package/templates/chat-app/components/ui/badge.tsx +22 -22
  56. package/templates/chat-app/components/ui/breadcrumb.tsx +72 -72
  57. package/templates/chat-app/components/ui/button-group.tsx +58 -58
  58. package/templates/chat-app/components/ui/button.tsx +45 -45
  59. package/templates/chat-app/components/ui/card.tsx +65 -65
  60. package/templates/chat-app/components/ui/checkbox.tsx +16 -16
  61. package/templates/chat-app/components/ui/collapsible.tsx +1 -1
  62. package/templates/chat-app/components/ui/command.tsx +137 -137
  63. package/templates/chat-app/components/ui/dialog.tsx +94 -94
  64. package/templates/chat-app/components/ui/drawer.tsx +68 -68
  65. package/templates/chat-app/components/ui/dropdown-menu.tsx +184 -184
  66. package/templates/chat-app/components/ui/empty.tsx +76 -76
  67. package/templates/chat-app/components/ui/extra/action-container.tsx +3 -3
  68. package/templates/chat-app/components/ui/extra/scroll-area-viewport-ref.tsx +24 -24
  69. package/templates/chat-app/components/ui/form.tsx +112 -112
  70. package/templates/chat-app/components/ui/hover-card.tsx +25 -25
  71. package/templates/chat-app/components/ui/input-group.tsx +126 -126
  72. package/templates/chat-app/components/ui/input.tsx +13 -13
  73. package/templates/chat-app/components/ui/label.tsx +12 -12
  74. package/templates/chat-app/components/ui/popover.tsx +25 -25
  75. package/templates/chat-app/components/ui/progress.tsx +19 -19
  76. package/templates/chat-app/components/ui/resizable.tsx +27 -27
  77. package/templates/chat-app/components/ui/scroll-area.tsx +30 -30
  78. package/templates/chat-app/components/ui/select.tsx +108 -108
  79. package/templates/chat-app/components/ui/separator.tsx +16 -16
  80. package/templates/chat-app/components/ui/sheet.tsx +91 -91
  81. package/templates/chat-app/components/ui/sidebar.tsx +615 -615
  82. package/templates/chat-app/components/ui/skeleton.tsx +7 -7
  83. package/templates/chat-app/components/ui/slider.tsx +50 -50
  84. package/templates/chat-app/components/ui/spinner.tsx +8 -8
  85. package/templates/chat-app/components/ui/switch.tsx +16 -16
  86. package/templates/chat-app/components/ui/table.tsx +71 -71
  87. package/templates/chat-app/components/ui/tabs.tsx +31 -31
  88. package/templates/chat-app/components/ui/textarea.tsx +10 -10
  89. package/templates/chat-app/components/ui/toggle.tsx +31 -31
  90. package/templates/chat-app/components/ui/tooltip.tsx +48 -48
  91. package/templates/chat-app/components/upgrade-cta/limit-display.tsx +7 -7
  92. package/templates/chat-app/components/upgrade-cta/login-cta-banner.tsx +3 -3
  93. package/templates/chat-app/components/upgrade-cta/login-prompt.tsx +3 -3
  94. package/templates/chat-app/hooks/use-mobile.ts +13 -13
  95. package/templates/chat-app/lib/ai/core-chat-agent.ts +25 -14
  96. package/templates/chat-app/lib/ai/eval-agent.ts +4 -5
  97. package/templates/chat-app/lib/ai/installed-tools.ts +12 -0
  98. package/templates/chat-app/lib/ai/mcp/mcp-client.ts +2 -2
  99. package/templates/chat-app/lib/ai/models.generated.ts +4236 -4585
  100. package/templates/chat-app/lib/ai/tool-renderer-registry.ts +31 -0
  101. package/templates/chat-app/lib/ai/types.ts +15 -20
  102. package/templates/chat-app/lib/config-requirements.ts +11 -6
  103. package/templates/chat-app/lib/config-schema.ts +13 -0
  104. package/templates/chat-app/lib/stores/hooks-message-parts.ts +1 -1
  105. package/templates/chat-app/lib/utils.ts +157 -157
  106. package/templates/chat-app/package.json +1 -1
  107. package/templates/chat-app/scripts/check-env.ts +229 -2
  108. package/templates/chat-app/tools/chatjs/_shared/lib/tool-part.ts +5 -0
  109. package/templates/chat-app/{components/part/weather.tsx → tools/chatjs/get-weather/renderer.tsx} +24 -38
  110. package/templates/chat-app/{components/part/retrieve-url.tsx → tools/chatjs/retrieve-url/renderer.tsx} +20 -15
  111. package/templates/chat-app/{lib/ai/tools/retrieve-url.ts → tools/chatjs/retrieve-url/tool.ts} +46 -7
  112. package/templates/chat-app/tools/chatjs/tools.ts +16 -0
  113. package/templates/chat-app/tools/chatjs/ui.ts +17 -0
  114. package/templates/chat-app/tools/chatjs/word-count/renderer.tsx +50 -0
  115. package/templates/chat-app/tools/chatjs/word-count/tool.ts +30 -0
  116. package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.ts +3 -5
  117. package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/deep-research.ts +2 -3
  118. package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/pipeline.ts +1 -1
  119. package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/types.ts +1 -1
  120. package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/utils.ts +7 -7
  121. package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/types.ts +1 -1
  122. package/templates/chat-app/{lib/ai/tools → tools/platform}/generate-video.ts +4 -6
  123. package/templates/chat-app/{lib/ai/tools → tools/platform}/read-document.ts +2 -2
  124. package/templates/chat-app/{lib/ai/tools → tools/platform}/steps/multi-query-web-search.ts +1 -1
  125. package/templates/chat-app/{lib/ai/tools → tools/platform}/steps/web-search.ts +1 -1
  126. package/templates/chat-app/{lib/ai/tools → tools/platform}/tools.ts +20 -20
  127. package/templates/chat-app/{lib/ai/tools → tools/platform}/web-search.ts +7 -5
  128. package/templates/electron/CHANGELOG.md +7 -1
  129. package/templates/electron/package.json +1 -1
  130. package/templates/chat-app/lib/ai/tools/tools-definitions.ts +0 -83
  131. /package/templates/chat-app/{lib/ai/tools/get-weather.ts → tools/chatjs/get-weather/tool.ts} +0 -0
  132. /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.javascript.ts +0 -0
  133. /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.python.ts +0 -0
  134. /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.shared.test.ts +0 -0
  135. /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.shared.ts +0 -0
  136. /package/templates/chat-app/{lib/ai/tools → tools/platform}/code-execution.types.ts +0 -0
  137. /package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/configuration.ts +0 -0
  138. /package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/prompts.ts +0 -0
  139. /package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/researcher-agent.ts +0 -0
  140. /package/templates/chat-app/{lib/ai/tools → tools/platform}/deep-research/supervisor-agent.ts +0 -0
  141. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/code-guidelines.ts +0 -0
  142. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/create-code-document.ts +0 -0
  143. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/create-sheet-document.ts +0 -0
  144. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/create-text-document.ts +0 -0
  145. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/edit-code-document.ts +0 -0
  146. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/edit-sheet-document.ts +0 -0
  147. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/edit-text-document.ts +0 -0
  148. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/sheet-guidelines.ts +0 -0
  149. /package/templates/chat-app/{lib/ai/tools → tools/platform}/documents/text-guidelines.ts +0 -0
  150. /package/templates/chat-app/{lib/ai/tools → tools/platform}/generate-image.ts +0 -0
  151. /package/templates/chat-app/{lib/ai/tools → tools/platform}/research-updates-schema.ts +0 -0
  152. /package/templates/chat-app/{lib/ai/tools → tools/platform}/steps/search-utils.ts +0 -0
  153. /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();
@@ -0,0 +1,5 @@
1
+ import type { Tool, UIToolInvocation } from "ai";
2
+
3
+ export type ToolPartFromTool<T extends Tool> = UIToolInvocation<T>;
4
+
5
+ export type TypelessToolPartFromTool<T extends Tool> = ToolPartFromTool<T>;
@@ -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
- "bg-blue-400": isDay,
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
- "bg-yellow-300": isDay,
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
- "bg-yellow-300": isDay,
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 Weather({ tool }: { tool: WeatherTool }) {
272
- const isLoading = tool.state === "input-available";
273
- const weatherAtLocation: WeatherAtLocation =
274
- tool.state === "output-available" ? tool.output : SAMPLE;
275
-
276
- if (isLoading) {
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={weatherAtLocation} />
261
+ <WeatherCard weatherAtLocation={SAMPLE} />
280
262
  </div>
281
263
  );
282
264
  }
283
265
 
284
- return <WeatherCard weatherAtLocation={weatherAtLocation} />;
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 { ChatMessage } from "@/lib/ai/types";
5
+ import type { TypelessToolPartFromTool } from "@/tools/chatjs/_shared/lib/tool-part";
6
+ import type { retrieveUrl } from "./tool";
7
7
 
8
- export type RetrieveUrlTool = Extract<
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
- <Image
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 RetrieveUrl({ tool }: { tool: RetrieveUrlTool }) {
163
- if (tool.state === "input-available") {
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);
@@ -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 "../../logger";
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 content = await app.scrapeUrl(url);
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([url], {
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: content.metadata.sourceURL,
109
+ url: redactedUrl,
75
110
  description: description || "",
76
111
  language: content.metadata.language,
77
112
  },
78
113
  ],
79
114
  };
80
115
  } catch (error) {
81
- log.error({ err: error, url }, "Firecrawl API error in retrieveUrl tool");
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
- import { toolsDefinitions } from "./tools-definitions";
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) {