@framers/agentos-ext-weather 1.0.0 → 1.1.0

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/src/index.ts DELETED
@@ -1,60 +0,0 @@
1
- import type {
2
- ExtensionPackContext,
3
- ExtensionPack,
4
- ExtensionLifecycleContext,
5
- } from '@framers/agentos';
6
-
7
- import { WeatherService } from './services/weatherService.js';
8
- import { WeatherLookupTool } from './tools/weatherLookup.js';
9
-
10
- export interface WeatherExtensionOptions {
11
- weatherApiKey?: string;
12
- priority?: number;
13
- }
14
-
15
- export function createExtensionPack(context: ExtensionPackContext): ExtensionPack {
16
- const options = (context.options ?? {}) as WeatherExtensionOptions;
17
-
18
- const weatherApiKey =
19
- options.weatherApiKey ||
20
- context.getSecret?.('weather.weatherApiKey') ||
21
- process.env.WEATHERAPI_KEY;
22
-
23
- const service = new WeatherService(weatherApiKey);
24
- const tool = new WeatherLookupTool(service);
25
-
26
- return {
27
- name: '@framers/agentos-ext-weather',
28
- version: '1.0.0',
29
- descriptors: [
30
- {
31
- id: tool.name,
32
- kind: 'tool',
33
- priority: options.priority ?? 50,
34
- payload: tool,
35
- requiredSecrets: [{ id: 'weather.weatherApiKey', optional: true }],
36
- },
37
- ],
38
- onActivate: async (lc?: ExtensionLifecycleContext) => {
39
- const provider = service.hasWeatherApiKey
40
- ? 'WeatherAPI.com (preferred) + Open-Meteo fallback'
41
- : 'Open-Meteo only (set WEATHERAPI_KEY for WeatherAPI.com)';
42
- lc?.logger?.info(`Weather Extension activated — ${provider}`);
43
- },
44
- onDeactivate: async (lc?: ExtensionLifecycleContext) => {
45
- lc?.logger?.info('Weather Extension deactivated');
46
- },
47
- };
48
- }
49
-
50
- export { WeatherService } from './services/weatherService.js';
51
- export { WeatherLookupTool } from './tools/weatherLookup.js';
52
- export type {
53
- WeatherResult,
54
- GeoLocation,
55
- CurrentWeather,
56
- ForecastDay,
57
- WeatherAlert,
58
- } from './services/weatherService.js';
59
- export type { WeatherLookupInput } from './tools/weatherLookup.js';
60
- export default createExtensionPack;
@@ -1,275 +0,0 @@
1
- /**
2
- * Dual-provider weather service.
3
- *
4
- * Provider priority:
5
- * 1. WeatherAPI.com — preferred when WEATHERAPI_KEY is configured
6
- * 2. Open-Meteo — always-available fallback (no key required)
7
- *
8
- * Geocoding is handled by the Open-Meteo Geocoding API (free, no key).
9
- */
10
-
11
- // ── Types ──────────────────────────────────────────────────────────────────
12
-
13
- export interface GeoLocation {
14
- name: string;
15
- region: string;
16
- country: string;
17
- lat: number;
18
- lon: number;
19
- timezone: string;
20
- }
21
-
22
- export interface CurrentWeather {
23
- temp_f: number;
24
- temp_c: number;
25
- condition: string;
26
- humidity: number;
27
- wind_mph: number;
28
- wind_dir: string;
29
- uv: number;
30
- feels_like_f: number;
31
- }
32
-
33
- export interface ForecastDay {
34
- date: string;
35
- max_f: number;
36
- min_f: number;
37
- max_c: number;
38
- min_c: number;
39
- condition: string;
40
- precip_chance: number;
41
- wind_max_mph: number;
42
- uv_max: number;
43
- }
44
-
45
- export interface WeatherAlert {
46
- headline: string;
47
- severity: string;
48
- description: string;
49
- }
50
-
51
- export interface WeatherResult {
52
- provider: 'weatherapi' | 'open-meteo';
53
- location: GeoLocation;
54
- current?: CurrentWeather;
55
- forecast: ForecastDay[];
56
- alerts?: WeatherAlert[];
57
- }
58
-
59
- // ── WMO weather code → human-readable condition ────────────────────────────
60
-
61
- const WMO_CODES: Record<number, string> = {
62
- 0: 'Clear sky',
63
- 1: 'Mainly clear',
64
- 2: 'Partly cloudy',
65
- 3: 'Overcast',
66
- 45: 'Fog',
67
- 48: 'Depositing rime fog',
68
- 51: 'Light drizzle',
69
- 53: 'Moderate drizzle',
70
- 55: 'Dense drizzle',
71
- 56: 'Light freezing drizzle',
72
- 57: 'Dense freezing drizzle',
73
- 61: 'Slight rain',
74
- 63: 'Moderate rain',
75
- 65: 'Heavy rain',
76
- 66: 'Light freezing rain',
77
- 67: 'Heavy freezing rain',
78
- 71: 'Slight snow',
79
- 73: 'Moderate snow',
80
- 75: 'Heavy snow',
81
- 77: 'Snow grains',
82
- 80: 'Slight rain showers',
83
- 81: 'Moderate rain showers',
84
- 82: 'Violent rain showers',
85
- 85: 'Slight snow showers',
86
- 86: 'Heavy snow showers',
87
- 95: 'Thunderstorm',
88
- 96: 'Thunderstorm with slight hail',
89
- 99: 'Thunderstorm with heavy hail',
90
- };
91
-
92
- function wmoToCondition(code: number): string {
93
- return WMO_CODES[code] ?? `Unknown (${code})`;
94
- }
95
-
96
- /** Convert Fahrenheit to Celsius, rounded to 1 decimal. */
97
- function fToC(f: number): number {
98
- return Math.round(((f - 32) * 5) / 9 * 10) / 10;
99
- }
100
-
101
- // ── Service ────────────────────────────────────────────────────────────────
102
-
103
- export class WeatherService {
104
- constructor(private readonly weatherApiKey?: string) {}
105
-
106
- get hasWeatherApiKey(): boolean {
107
- return !!this.weatherApiKey;
108
- }
109
-
110
- /** Geocode a location string to coordinates via Open-Meteo. */
111
- async geocode(location: string): Promise<GeoLocation> {
112
- const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1&language=en&format=json`;
113
- const res = await fetch(url);
114
- if (!res.ok) {
115
- throw new Error(`Geocoding failed (${res.status}): ${res.statusText}`);
116
- }
117
-
118
- const data = (await res.json()) as any;
119
- const results = data.results;
120
- if (!results || results.length === 0) {
121
- throw new Error(`Location not found: "${location}"`);
122
- }
123
-
124
- const r = results[0];
125
- return {
126
- name: r.name,
127
- region: r.admin1 ?? '',
128
- country: r.country ?? '',
129
- lat: r.latitude,
130
- lon: r.longitude,
131
- timezone: r.timezone ?? 'UTC',
132
- };
133
- }
134
-
135
- /** Main entry: get weather for a location string. */
136
- async getWeather(location: string, days: number = 3): Promise<WeatherResult> {
137
- const geo = await this.geocode(location);
138
-
139
- // Try WeatherAPI.com first if key is available
140
- if (this.weatherApiKey) {
141
- try {
142
- return await this.fetchWeatherApi(geo, days);
143
- } catch {
144
- // Fall through to Open-Meteo
145
- }
146
- }
147
-
148
- return this.fetchOpenMeteo(geo, days);
149
- }
150
-
151
- // ── WeatherAPI.com ─────────────────────────────────────────────────────
152
-
153
- private async fetchWeatherApi(geo: GeoLocation, days: number): Promise<WeatherResult> {
154
- const clampedDays = Math.min(days, 3); // free tier = 3 days max
155
- const q = `${geo.lat},${geo.lon}`;
156
- const url = `https://api.weatherapi.com/v1/forecast.json?key=${this.weatherApiKey}&q=${q}&days=${clampedDays}&aqi=no&alerts=yes`;
157
-
158
- const res = await fetch(url);
159
- if (!res.ok) {
160
- const body = await res.text().catch(() => '');
161
- throw new Error(`WeatherAPI error (${res.status}): ${body}`);
162
- }
163
-
164
- const data = (await res.json()) as any;
165
- const loc = data.location ?? {};
166
- const cur = data.current ?? {};
167
- const forecastDays: ForecastDay[] = (data.forecast?.forecastday ?? []).map((fd: any) => ({
168
- date: fd.date,
169
- max_f: fd.day?.maxtemp_f ?? 0,
170
- min_f: fd.day?.mintemp_f ?? 0,
171
- max_c: fd.day?.maxtemp_c ?? 0,
172
- min_c: fd.day?.mintemp_c ?? 0,
173
- condition: fd.day?.condition?.text ?? 'Unknown',
174
- precip_chance: fd.day?.daily_chance_of_rain ?? 0,
175
- wind_max_mph: fd.day?.maxwind_mph ?? 0,
176
- uv_max: fd.day?.uv ?? 0,
177
- }));
178
-
179
- const alerts: WeatherAlert[] = (data.alerts?.alert ?? []).map((a: any) => ({
180
- headline: a.headline ?? '',
181
- severity: a.severity ?? '',
182
- description: a.desc ?? a.event ?? '',
183
- }));
184
-
185
- return {
186
- provider: 'weatherapi',
187
- location: {
188
- name: loc.name ?? geo.name,
189
- region: loc.region ?? geo.region,
190
- country: loc.country ?? geo.country,
191
- lat: loc.lat ?? geo.lat,
192
- lon: loc.lon ?? geo.lon,
193
- timezone: loc.tz_id ?? geo.timezone,
194
- },
195
- current: {
196
- temp_f: cur.temp_f ?? 0,
197
- temp_c: cur.temp_c ?? 0,
198
- condition: cur.condition?.text ?? 'Unknown',
199
- humidity: cur.humidity ?? 0,
200
- wind_mph: cur.wind_mph ?? 0,
201
- wind_dir: cur.wind_dir ?? '',
202
- uv: cur.uv ?? 0,
203
- feels_like_f: cur.feelslike_f ?? 0,
204
- },
205
- forecast: forecastDays,
206
- alerts: alerts.length > 0 ? alerts : undefined,
207
- };
208
- }
209
-
210
- // ── Open-Meteo ─────────────────────────────────────────────────────────
211
-
212
- private async fetchOpenMeteo(geo: GeoLocation, days: number): Promise<WeatherResult> {
213
- const clampedDays = Math.min(days, 16);
214
- const hourlyVars = 'temperature_2m,relative_humidity_2m,wind_speed_10m,precipitation_probability,weather_code';
215
- const dailyVars = 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,wind_speed_10m_max,uv_index_max';
216
- const url =
217
- `https://api.open-meteo.com/v1/forecast` +
218
- `?latitude=${geo.lat}&longitude=${geo.lon}` +
219
- `&hourly=${hourlyVars}` +
220
- `&daily=${dailyVars}` +
221
- `&current=temperature_2m,relative_humidity_2m,wind_speed_10m,wind_direction_10m,weather_code` +
222
- `&temperature_unit=fahrenheit` +
223
- `&wind_speed_unit=mph` +
224
- `&timezone=auto` +
225
- `&forecast_days=${clampedDays}`;
226
-
227
- const res = await fetch(url);
228
- if (!res.ok) {
229
- throw new Error(`Open-Meteo error (${res.status}): ${res.statusText}`);
230
- }
231
-
232
- const data = (await res.json()) as any;
233
- const cur = data.current ?? {};
234
- const daily = data.daily ?? {};
235
-
236
- const forecastDays: ForecastDay[] = [];
237
- const dates: string[] = daily.time ?? [];
238
- for (let i = 0; i < dates.length; i++) {
239
- const maxF = daily.temperature_2m_max?.[i] ?? 0;
240
- const minF = daily.temperature_2m_min?.[i] ?? 0;
241
- forecastDays.push({
242
- date: dates[i],
243
- max_f: maxF,
244
- min_f: minF,
245
- max_c: fToC(maxF),
246
- min_c: fToC(minF),
247
- condition: wmoToCondition(daily.weather_code?.[i] ?? 0),
248
- precip_chance: daily.precipitation_probability_max?.[i] ?? 0,
249
- wind_max_mph: daily.wind_speed_10m_max?.[i] ?? 0,
250
- uv_max: daily.uv_index_max?.[i] ?? 0,
251
- });
252
- }
253
-
254
- const tempF = cur.temperature_2m ?? 0;
255
- const windDir = cur.wind_direction_10m ?? 0;
256
- const cardinals = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
257
- const windDirStr = cardinals[Math.round(windDir / 22.5) % 16];
258
-
259
- return {
260
- provider: 'open-meteo',
261
- location: geo,
262
- current: {
263
- temp_f: tempF,
264
- temp_c: fToC(tempF),
265
- condition: wmoToCondition(cur.weather_code ?? 0),
266
- humidity: cur.relative_humidity_2m ?? 0,
267
- wind_mph: cur.wind_speed_10m ?? 0,
268
- wind_dir: windDirStr,
269
- uv: forecastDays[0]?.uv_max ?? 0,
270
- feels_like_f: tempF, // Open-Meteo free doesn't provide feels-like
271
- },
272
- forecast: forecastDays,
273
- };
274
- }
275
- }
@@ -1,77 +0,0 @@
1
- import type {
2
- ITool,
3
- ToolExecutionContext,
4
- ToolExecutionResult,
5
- JSONSchemaObject,
6
- } from '@framers/agentos';
7
-
8
- import { WeatherService } from '../services/weatherService.js';
9
- import type { WeatherResult } from '../services/weatherService.js';
10
-
11
- export interface WeatherLookupInput {
12
- location: string;
13
- days?: number;
14
- }
15
-
16
- export class WeatherLookupTool implements ITool<WeatherLookupInput, WeatherResult> {
17
- readonly id = 'weather-lookup-v1';
18
- readonly name = 'weather_lookup';
19
- readonly displayName = 'Weather Lookup';
20
- readonly description =
21
- 'Get current weather conditions and forecasts for any location. Returns temperature (F/C), humidity, wind, precipitation, UV index, and severe weather alerts.';
22
- readonly category = 'research';
23
- readonly version = '1.0.0';
24
- readonly hasSideEffects = false;
25
-
26
- readonly inputSchema: JSONSchemaObject = {
27
- type: 'object',
28
- required: ['location'],
29
- properties: {
30
- location: {
31
- type: 'string',
32
- description: 'City name, zip code, or lat,lon coordinates (e.g. "Las Vegas", "90210", "36.17,-115.14")',
33
- },
34
- days: {
35
- type: 'integer',
36
- minimum: 1,
37
- maximum: 14,
38
- default: 3,
39
- description: 'Number of forecast days (1-14, default 3)',
40
- },
41
- },
42
- additionalProperties: false,
43
- };
44
-
45
- readonly requiredCapabilities = ['capability:web_access'];
46
-
47
- constructor(private readonly service: WeatherService) {}
48
-
49
- async execute(
50
- args: WeatherLookupInput,
51
- _context: ToolExecutionContext,
52
- ): Promise<ToolExecutionResult<WeatherResult>> {
53
- if (!args.location || args.location.trim().length === 0) {
54
- return { success: false, error: 'Location is required.' };
55
- }
56
-
57
- try {
58
- const result = await this.service.getWeather(args.location.trim(), args.days ?? 3);
59
- return { success: true, output: result };
60
- } catch (err: any) {
61
- return { success: false, error: `Weather lookup failed: ${err.message}` };
62
- }
63
- }
64
-
65
- validateArgs(input: Record<string, any>): { isValid: boolean; errors?: any[] } {
66
- const errors: string[] = [];
67
- if (!input.location || typeof input.location !== 'string') {
68
- errors.push('location is required and must be a string');
69
- }
70
- if (input.days !== undefined) {
71
- if (typeof input.days !== 'number' || input.days < 1 || input.days > 14) {
72
- errors.push('days must be an integer between 1 and 14');
73
- }
74
- }
75
- return { isValid: errors.length === 0, errors: errors.length ? errors : undefined };
76
- }
77
- }
@@ -1,270 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
-
3
- const mockFetch = vi.fn();
4
- vi.stubGlobal('fetch', mockFetch);
5
-
6
- // Dynamic import after stubbing fetch
7
- const { WeatherService } = await import('../src/services/weatherService.js');
8
- const { WeatherLookupTool } = await import('../src/tools/weatherLookup.js');
9
- const { createExtensionPack } = await import('../src/index.js');
10
-
11
- // ── Fixtures ──────────────────────────────────────────────────────────────
12
-
13
- const GEOCODE_RESPONSE = {
14
- results: [
15
- {
16
- name: 'Las Vegas',
17
- admin1: 'Nevada',
18
- country: 'United States',
19
- latitude: 36.175,
20
- longitude: -115.1372,
21
- timezone: 'America/Los_Angeles',
22
- },
23
- ],
24
- };
25
-
26
- const WEATHERAPI_RESPONSE = {
27
- location: {
28
- name: 'Las Vegas',
29
- region: 'Nevada',
30
- country: 'United States of America',
31
- lat: 36.17,
32
- lon: -115.14,
33
- tz_id: 'America/Los_Angeles',
34
- },
35
- current: {
36
- temp_f: 95.0,
37
- temp_c: 35.0,
38
- condition: { text: 'Sunny' },
39
- humidity: 12,
40
- wind_mph: 8.1,
41
- wind_dir: 'SW',
42
- uv: 9.0,
43
- feelslike_f: 93.0,
44
- },
45
- forecast: {
46
- forecastday: [
47
- {
48
- date: '2026-02-23',
49
- day: {
50
- maxtemp_f: 97.0,
51
- mintemp_f: 72.0,
52
- maxtemp_c: 36.1,
53
- mintemp_c: 22.2,
54
- condition: { text: 'Sunny' },
55
- daily_chance_of_rain: 0,
56
- maxwind_mph: 12.5,
57
- uv: 9.0,
58
- },
59
- },
60
- ],
61
- },
62
- alerts: { alert: [] },
63
- };
64
-
65
- const OPENMETEO_RESPONSE = {
66
- latitude: 36.175,
67
- longitude: -115.1372,
68
- timezone: 'America/Los_Angeles',
69
- current: {
70
- temperature_2m: 92.0,
71
- relative_humidity_2m: 15,
72
- wind_speed_10m: 7.5,
73
- wind_direction_10m: 225,
74
- weather_code: 0,
75
- },
76
- daily: {
77
- time: ['2026-02-23', '2026-02-24', '2026-02-25'],
78
- weather_code: [0, 1, 3],
79
- temperature_2m_max: [96.0, 94.0, 90.0],
80
- temperature_2m_min: [71.0, 70.0, 68.0],
81
- precipitation_sum: [0, 0, 0.1],
82
- precipitation_probability_max: [0, 5, 20],
83
- wind_speed_10m_max: [12.0, 10.0, 15.0],
84
- uv_index_max: [9.0, 8.5, 7.0],
85
- },
86
- };
87
-
88
- // ── Tests ─────────────────────────────────────────────────────────────────
89
-
90
- describe('WeatherService', () => {
91
- beforeEach(() => {
92
- vi.clearAllMocks();
93
- });
94
-
95
- describe('geocode', () => {
96
- it('resolves a city name to coordinates', async () => {
97
- mockFetch.mockResolvedValueOnce({
98
- ok: true,
99
- json: async () => GEOCODE_RESPONSE,
100
- });
101
-
102
- const service = new WeatherService();
103
- const geo = await service.geocode('Las Vegas');
104
-
105
- expect(geo.name).toBe('Las Vegas');
106
- expect(geo.lat).toBe(36.175);
107
- expect(geo.lon).toBe(-115.1372);
108
- expect(geo.region).toBe('Nevada');
109
- });
110
-
111
- it('throws on empty results', async () => {
112
- mockFetch.mockResolvedValueOnce({
113
- ok: true,
114
- json: async () => ({ results: [] }),
115
- });
116
-
117
- const service = new WeatherService();
118
- await expect(service.geocode('xyznonexistent')).rejects.toThrow('Location not found');
119
- });
120
- });
121
-
122
- describe('getWeather with WeatherAPI.com', () => {
123
- it('uses WeatherAPI when key is provided', async () => {
124
- // geocode call
125
- mockFetch.mockResolvedValueOnce({
126
- ok: true,
127
- json: async () => GEOCODE_RESPONSE,
128
- });
129
- // weatherapi call
130
- mockFetch.mockResolvedValueOnce({
131
- ok: true,
132
- json: async () => WEATHERAPI_RESPONSE,
133
- });
134
-
135
- const service = new WeatherService('test-key-123');
136
- const result = await service.getWeather('Las Vegas', 3);
137
-
138
- expect(result.provider).toBe('weatherapi');
139
- expect(result.current?.temp_f).toBe(95.0);
140
- expect(result.current?.condition).toBe('Sunny');
141
- expect(result.forecast).toHaveLength(1);
142
- expect(result.forecast[0].max_f).toBe(97.0);
143
- });
144
-
145
- it('falls back to Open-Meteo when WeatherAPI fails', async () => {
146
- // geocode call
147
- mockFetch.mockResolvedValueOnce({
148
- ok: true,
149
- json: async () => GEOCODE_RESPONSE,
150
- });
151
- // weatherapi fails
152
- mockFetch.mockResolvedValueOnce({
153
- ok: false,
154
- status: 403,
155
- text: async () => 'Forbidden',
156
- });
157
- // open-meteo call
158
- mockFetch.mockResolvedValueOnce({
159
- ok: true,
160
- json: async () => OPENMETEO_RESPONSE,
161
- });
162
-
163
- const service = new WeatherService('bad-key');
164
- const result = await service.getWeather('Las Vegas', 3);
165
-
166
- expect(result.provider).toBe('open-meteo');
167
- expect(result.current?.temp_f).toBe(92.0);
168
- expect(result.forecast).toHaveLength(3);
169
- });
170
- });
171
-
172
- describe('getWeather with Open-Meteo fallback', () => {
173
- it('uses Open-Meteo when no key is provided', async () => {
174
- // geocode call
175
- mockFetch.mockResolvedValueOnce({
176
- ok: true,
177
- json: async () => GEOCODE_RESPONSE,
178
- });
179
- // open-meteo call
180
- mockFetch.mockResolvedValueOnce({
181
- ok: true,
182
- json: async () => OPENMETEO_RESPONSE,
183
- });
184
-
185
- const service = new WeatherService();
186
- const result = await service.getWeather('Las Vegas', 3);
187
-
188
- expect(result.provider).toBe('open-meteo');
189
- expect(result.location.name).toBe('Las Vegas');
190
- expect(result.current?.humidity).toBe(15);
191
- expect(result.current?.wind_dir).toBe('SW');
192
- expect(result.forecast).toHaveLength(3);
193
- expect(result.forecast[0].condition).toBe('Clear sky');
194
- expect(result.forecast[1].condition).toBe('Mainly clear');
195
- expect(result.forecast[2].condition).toBe('Overcast');
196
- });
197
- });
198
- });
199
-
200
- describe('WeatherLookupTool', () => {
201
- let tool: InstanceType<typeof WeatherLookupTool>;
202
-
203
- beforeEach(() => {
204
- vi.clearAllMocks();
205
- const service = new WeatherService('test-key');
206
- tool = new WeatherLookupTool(service);
207
- });
208
-
209
- it('has correct metadata', () => {
210
- expect(tool.id).toBe('weather-lookup-v1');
211
- expect(tool.name).toBe('weather_lookup');
212
- expect(tool.category).toBe('research');
213
- expect(tool.hasSideEffects).toBe(false);
214
- });
215
-
216
- it('has valid input schema', () => {
217
- expect(tool.inputSchema.type).toBe('object');
218
- expect(tool.inputSchema.required).toContain('location');
219
- expect(tool.inputSchema.properties).toHaveProperty('location');
220
- expect(tool.inputSchema.properties).toHaveProperty('days');
221
- });
222
-
223
- it('validates args correctly', () => {
224
- expect(tool.validateArgs({ location: 'Las Vegas' }).isValid).toBe(true);
225
- expect(tool.validateArgs({ location: 'NYC', days: 5 }).isValid).toBe(true);
226
- expect(tool.validateArgs({}).isValid).toBe(false);
227
- expect(tool.validateArgs({ location: 'NYC', days: 20 }).isValid).toBe(false);
228
- });
229
-
230
- it('returns error for empty location', async () => {
231
- const result = await tool.execute({ location: '' }, {} as any);
232
- expect(result.success).toBe(false);
233
- expect(result.error).toContain('required');
234
- });
235
-
236
- it('executes successfully with mocked service', async () => {
237
- // geocode
238
- mockFetch.mockResolvedValueOnce({
239
- ok: true,
240
- json: async () => GEOCODE_RESPONSE,
241
- });
242
- // weatherapi
243
- mockFetch.mockResolvedValueOnce({
244
- ok: true,
245
- json: async () => WEATHERAPI_RESPONSE,
246
- });
247
-
248
- const result = await tool.execute({ location: 'Las Vegas', days: 3 }, {} as any);
249
- expect(result.success).toBe(true);
250
- expect(result.output?.provider).toBe('weatherapi');
251
- expect(result.output?.location.name).toBe('Las Vegas');
252
- });
253
- });
254
-
255
- describe('createExtensionPack', () => {
256
- it('creates pack with correct metadata', () => {
257
- const pack = createExtensionPack({
258
- options: { weatherApiKey: 'test' },
259
- } as any);
260
- expect(pack.name).toBe('@framers/agentos-ext-weather');
261
- expect(pack.descriptors).toHaveLength(1);
262
- expect(pack.descriptors[0].id).toBe('weather_lookup');
263
- expect(pack.descriptors[0].kind).toBe('tool');
264
- });
265
-
266
- it('works without API key', () => {
267
- const pack = createExtensionPack({ options: {} } as any);
268
- expect(pack.descriptors).toHaveLength(1);
269
- });
270
- });
package/tsconfig.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "lib": ["ES2022"],
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "noEmitOnError": false,
12
- "forceConsistentCasingInFileNames": true,
13
- "declaration": true,
14
- "declarationMap": true,
15
- "sourceMap": true,
16
- "resolveJsonModule": true,
17
- "moduleResolution": "Bundler"
18
- },
19
- "include": ["src/**/*"],
20
- "exclude": ["node_modules", "dist", "test"]
21
- }