@riverintel/stayfinder-plugin 0.2.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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +94 -0
  3. package/dist/adapter-client.d.ts +68 -0
  4. package/dist/adapter-client.d.ts.map +1 -0
  5. package/dist/adapter-client.js +149 -0
  6. package/dist/adapter-client.js.map +1 -0
  7. package/dist/credential-store.d.ts +71 -0
  8. package/dist/credential-store.d.ts.map +1 -0
  9. package/dist/credential-store.js +143 -0
  10. package/dist/credential-store.js.map +1 -0
  11. package/dist/errors.d.ts +55 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +160 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +28 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/plugin-config.d.ts +37 -0
  20. package/dist/plugin-config.d.ts.map +1 -0
  21. package/dist/plugin-config.js +63 -0
  22. package/dist/plugin-config.js.map +1 -0
  23. package/dist/thumbnails.d.ts +31 -0
  24. package/dist/thumbnails.d.ts.map +1 -0
  25. package/dist/thumbnails.js +32 -0
  26. package/dist/thumbnails.js.map +1 -0
  27. package/dist/tool-result.d.ts +19 -0
  28. package/dist/tool-result.d.ts.map +1 -0
  29. package/dist/tool-result.js +18 -0
  30. package/dist/tool-result.js.map +1 -0
  31. package/dist/tools/search-stays.d.ts +45 -0
  32. package/dist/tools/search-stays.d.ts.map +1 -0
  33. package/dist/tools/search-stays.js +191 -0
  34. package/dist/tools/search-stays.js.map +1 -0
  35. package/dist/tools/stayfinder-signup.d.ts +38 -0
  36. package/dist/tools/stayfinder-signup.d.ts.map +1 -0
  37. package/dist/tools/stayfinder-signup.js +102 -0
  38. package/dist/tools/stayfinder-signup.js.map +1 -0
  39. package/dist/tools/stayfinder-verify.d.ts +26 -0
  40. package/dist/tools/stayfinder-verify.d.ts.map +1 -0
  41. package/dist/tools/stayfinder-verify.js +124 -0
  42. package/dist/tools/stayfinder-verify.js.map +1 -0
  43. package/dist/types.d.ts +193 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +17 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/validation.d.ts +51 -0
  48. package/dist/validation.d.ts.map +1 -0
  49. package/dist/validation.js +174 -0
  50. package/dist/validation.js.map +1 -0
  51. package/openclaw.plugin.json +41 -0
  52. package/package.json +87 -0
  53. package/skills/lodging-search/SKILL.md +235 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Shared TypeScript types for the StayFinder plugin.
3
+ *
4
+ * These mirror the public adapter API surface (the request/response shapes
5
+ * for `POST /v1/search/stays`, `POST /v1/signup`, `POST /v1/signup/verify`,
6
+ * `GET /v1/tenant/me`) and the on-disk credential store shape.
7
+ *
8
+ * They are deliberately a SUBSET of what the adapter returns — we only type
9
+ * the fields the plugin actually reads. The adapter is free to add new
10
+ * fields without breaking us; missing optional fields parse fine; unexpected
11
+ * extra fields are silently ignored at runtime.
12
+ *
13
+ * The single source of truth for the wire format is the adapter spec at
14
+ * docs/expedia-adapter-cloudrun-spec.md §7 (in the private operations repo).
15
+ */
16
+ /** ISO-8601 timestamp string, e.g. "2026-04-15T22:32:39.436Z". */
17
+ export type IsoTimestamp = string;
18
+ /** YYYY-MM-DD date string. */
19
+ export type IsoDate = string;
20
+ export interface StayFinderPluginConfig {
21
+ /** Base URL of the StayFinder service. Defaults to the public hosted endpoint. */
22
+ adapter_url: string;
23
+ /** ISO-3166-1 alpha-2 country code for point-of-sale. Defaults to 'US'. */
24
+ default_pos_country: string;
25
+ /** ISO-4217 currency code. If unset, the adapter falls back to the POS default. */
26
+ default_currency?: string;
27
+ /** HTTP timeout for adapter calls, in milliseconds. Defaults to 10000. */
28
+ request_timeout_ms: number;
29
+ }
30
+ export type LodgingType = 'hotel' | 'vacation_rental' | 'any';
31
+ export type SortOrder = 'recommended' | 'price_asc' | 'price_desc' | 'rating_desc' | 'distance';
32
+ export interface SearchStaysFilters {
33
+ pet_friendly?: boolean;
34
+ free_cancellation?: boolean;
35
+ min_star_rating?: number;
36
+ max_star_rating?: number;
37
+ price_min?: number;
38
+ price_max?: number;
39
+ }
40
+ export interface SearchStaysRequest {
41
+ destination: string;
42
+ check_in: IsoDate;
43
+ check_out: IsoDate;
44
+ adults: number;
45
+ children_ages?: number[];
46
+ lodging_type?: LodgingType;
47
+ filters?: SearchStaysFilters;
48
+ sort?: SortOrder;
49
+ limit?: number;
50
+ pos_country?: string;
51
+ currency?: string;
52
+ intent?: string;
53
+ hotel_name?: string;
54
+ }
55
+ /**
56
+ * Free-text fields that come back wrapped in
57
+ * <<<EXTERNAL_UNTRUSTED_CONTENT id="..."> ... <<<END_EXTERNAL_UNTRUSTED_CONTENT id="...">
58
+ * sentinels by the adapter. Typed as string here; the wrapping is plain
59
+ * text inside the string and the model is expected to recognize it.
60
+ */
61
+ export type WrappedString = string;
62
+ export interface SearchStaysProperty {
63
+ property_id: string;
64
+ name: WrappedString;
65
+ property_type?: string;
66
+ neighborhood?: WrappedString;
67
+ star_rating: number | null;
68
+ guest_rating?: {
69
+ score: number | null;
70
+ scale: number;
71
+ review_count: number;
72
+ };
73
+ price: {
74
+ amount_per_night: number;
75
+ amount_total: number;
76
+ currency: string;
77
+ taxes_included: boolean;
78
+ fees_estimate?: number;
79
+ };
80
+ free_cancellation?: boolean;
81
+ pet_friendly?: boolean;
82
+ descriptions?: {
83
+ location?: WrappedString;
84
+ hotel?: WrappedString;
85
+ room?: WrappedString;
86
+ };
87
+ distance?: {
88
+ value: number;
89
+ unit: 'km' | 'mi';
90
+ };
91
+ thumbnail_url?: string;
92
+ redirect_link: string;
93
+ geo?: {
94
+ lat: number;
95
+ lng: number;
96
+ obfuscated?: boolean;
97
+ };
98
+ }
99
+ export interface SearchStaysResponse {
100
+ request_id: string;
101
+ trace_id?: string;
102
+ cached: boolean;
103
+ cached_at: IsoTimestamp;
104
+ cache_ttl_seconds: number;
105
+ brand?: 'expedia' | 'vrbo';
106
+ resolved_destination?: {
107
+ id: string;
108
+ label: string;
109
+ type: string;
110
+ };
111
+ check_in: IsoDate;
112
+ check_out: IsoDate;
113
+ nights: number;
114
+ party: {
115
+ adults: number;
116
+ children: number;
117
+ };
118
+ currency: string;
119
+ result_count: number;
120
+ total_available?: number | null;
121
+ warnings: Array<{
122
+ code: string;
123
+ message: string;
124
+ }>;
125
+ results: SearchStaysProperty[];
126
+ }
127
+ export interface SignupResponse {
128
+ status: 'verification_sent';
129
+ message: string;
130
+ expires_in_seconds: number;
131
+ }
132
+ export interface SignupVerifyResponse {
133
+ status: 'verified';
134
+ tenant_id: string;
135
+ /** Plaintext API token — returned ONCE; never request twice. */
136
+ token: string;
137
+ token_kind: 'ephemeral' | 'persistent';
138
+ expires_at: IsoTimestamp | null;
139
+ quota_per_hour: number;
140
+ default_pos: string;
141
+ }
142
+ export interface TenantMeResponse {
143
+ tenant_id: string;
144
+ name: string | null;
145
+ email: string | null;
146
+ quota: {
147
+ limit_per_hour: number;
148
+ remaining: number;
149
+ reset_at: IsoTimestamp;
150
+ };
151
+ token: {
152
+ kind: 'ephemeral' | 'persistent';
153
+ expires_at: IsoTimestamp | null;
154
+ };
155
+ default_pos: string;
156
+ }
157
+ /**
158
+ * Every known adapter error code, in one place. The plugin maps each one
159
+ * to a model-facing message in errors.ts. Unknown codes still parse fine
160
+ * (the AdapterErrorEnvelope.code field is `string`, not this union) — the
161
+ * union is for exhaustive switch coverage in the mapper.
162
+ */
163
+ export type AdapterErrorCode = 'invalid_request' | 'missing_field' | 'invalid_email' | 'disposable_email' | 'code_invalid' | 'code_expired' | 'code_attempts_exceeded' | 'unauthorized' | 'token_expired' | 'tenant_suspended' | 'tenant_quota_exceeded' | 'global_quota_exceeded' | 'signup_rate_limited' | 'destination_not_found' | 'destination_ambiguous' | 'expedia_upstream_error' | 'upstream_timeout' | 'upstream_unavailable' | 'internal_error';
164
+ export interface AdapterErrorEnvelope {
165
+ error: {
166
+ code: string;
167
+ message: string;
168
+ request_id?: string;
169
+ trace_id?: string;
170
+ retry_after_seconds?: number;
171
+ attempts_remaining?: number;
172
+ expires_at?: string | null;
173
+ upstream_status?: number | null;
174
+ transaction_id?: string;
175
+ pos_country?: string;
176
+ details?: Record<string, unknown>;
177
+ };
178
+ }
179
+ export interface CredentialFile {
180
+ /** Plaintext API token, "oct_..." prefix. */
181
+ api_token: string;
182
+ /** ISO-8601 timestamp when the file was last written. */
183
+ saved_at: IsoTimestamp;
184
+ /** Tenant ID the token belongs to. */
185
+ tenant_id: string;
186
+ /** Email the tenant was registered under (used for re-auth). */
187
+ email: string;
188
+ /** Token kind: ephemeral tokens slide; persistent ones don't. */
189
+ token_kind: 'ephemeral' | 'persistent';
190
+ /** Token's current expires_at; null for persistent tokens. */
191
+ expires_at: IsoTimestamp | null;
192
+ }
193
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared TypeScript types for the StayFinder plugin.
3
+ *
4
+ * These mirror the public adapter API surface (the request/response shapes
5
+ * for `POST /v1/search/stays`, `POST /v1/signup`, `POST /v1/signup/verify`,
6
+ * `GET /v1/tenant/me`) and the on-disk credential store shape.
7
+ *
8
+ * They are deliberately a SUBSET of what the adapter returns — we only type
9
+ * the fields the plugin actually reads. The adapter is free to add new
10
+ * fields without breaking us; missing optional fields parse fine; unexpected
11
+ * extra fields are silently ignored at runtime.
12
+ *
13
+ * The single source of truth for the wire format is the adapter spec at
14
+ * docs/expedia-adapter-cloudrun-spec.md §7 (in the private operations repo).
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Pre-HTTP validation for `search_stays` parameters.
3
+ *
4
+ * The TypeBox schema in `tools/search-stays.ts` catches type and range
5
+ * errors that the runtime can enforce automatically (string vs number,
6
+ * minLength, maximum, enum membership, etc.). Anything that requires
7
+ * cross-field comparison or runtime computation lives here.
8
+ *
9
+ * Returning a non-null string from any of these functions means the
10
+ * tool's `execute` should throw with that string as the user-facing
11
+ * message instead of making the HTTP call. The string is phrased for
12
+ * the model — concrete next action, plain English, no error codes.
13
+ *
14
+ * The spec is explicit that error messages should give the model an
15
+ * actionable hypothesis. "check_out must be after check_in. Did you
16
+ * swap the dates?" is much better than "validation failed".
17
+ */
18
+ import type { SearchStaysRequest } from './types.js';
19
+ /**
20
+ * Run all post-schema validation checks against a `search_stays` request.
21
+ *
22
+ * Returns null if everything passes; otherwise returns the model-facing
23
+ * error message string. Tools call this in `execute` *before* hitting
24
+ * the HTTP layer so we never burn an adapter call on a request the
25
+ * adapter would also reject.
26
+ */
27
+ export declare function validateSearchStaysRequest(req: SearchStaysRequest, now?: Date): string | null;
28
+ /**
29
+ * Trim and validate an email address with a deliberately-loose check.
30
+ *
31
+ * The plugin doesn't try to be a perfect RFC 5322 validator — that's the
32
+ * adapter's job (it uses the same `email-validator` package and runs the
33
+ * disposable-domain check). All we want here is to reject things that
34
+ * obviously aren't emails before burning an HTTP call.
35
+ *
36
+ * Returns null on success, or a model-facing error string on failure.
37
+ */
38
+ export declare function validateEmailLoose(raw: string): string | null;
39
+ /**
40
+ * Strip whitespace, dashes, dots, and any non-digit characters from a
41
+ * pasted code, then return the result if it's exactly 6 ASCII digits.
42
+ *
43
+ * Returns null when the cleaned input isn't 6 digits — the verify tool
44
+ * uses this to short-circuit "the user pasted a token by mistake" or
45
+ * "the user pasted a phrase" without burning an HTTP call.
46
+ *
47
+ * Defensive cleanup matters because email clients sometimes paste
48
+ * non-breaking spaces or thin spaces around copied text.
49
+ */
50
+ export declare function sanitizeAndValidateCode(raw: unknown): string | null;
51
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAwCrD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,kBAAkB,EACvB,GAAG,GAAE,IAAiB,GACrB,MAAM,GAAG,IAAI,CAyFf;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoB7D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKnE"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Pre-HTTP validation for `search_stays` parameters.
3
+ *
4
+ * The TypeBox schema in `tools/search-stays.ts` catches type and range
5
+ * errors that the runtime can enforce automatically (string vs number,
6
+ * minLength, maximum, enum membership, etc.). Anything that requires
7
+ * cross-field comparison or runtime computation lives here.
8
+ *
9
+ * Returning a non-null string from any of these functions means the
10
+ * tool's `execute` should throw with that string as the user-facing
11
+ * message instead of making the HTTP call. The string is phrased for
12
+ * the model — concrete next action, plain English, no error codes.
13
+ *
14
+ * The spec is explicit that error messages should give the model an
15
+ * actionable hypothesis. "check_out must be after check_in. Did you
16
+ * swap the dates?" is much better than "validation failed".
17
+ */
18
+ const MAX_NIGHTS = 30;
19
+ const MAX_DAYS_OUT = 500;
20
+ const ymdRegex = /^\d{4}-\d{2}-\d{2}$/;
21
+ /**
22
+ * Parse a YYYY-MM-DD string as UTC midnight. Returns null if the string
23
+ * doesn't match the format or doesn't represent a real date.
24
+ *
25
+ * We use UTC because the adapter (and Expedia) treat the date as
26
+ * calendar-day-in-the-destination, not "wall clock at midnight in the
27
+ * user's local timezone". A user in NYC searching for a hotel in Tokyo
28
+ * for 2026-06-01 means 2026-06-01 in Tokyo, regardless of what time it
29
+ * is on their laptop.
30
+ */
31
+ const parseYmdUtc = (s) => {
32
+ if (!ymdRegex.test(s))
33
+ return null;
34
+ const ms = Date.parse(`${s}T00:00:00Z`);
35
+ if (!Number.isFinite(ms))
36
+ return null;
37
+ // Reject roll-overs like "2026-02-30" — Date.parse silently rolls them
38
+ // forward, so we round-trip and compare to catch the difference.
39
+ const back = new Date(ms).toISOString().slice(0, 10);
40
+ if (back !== s)
41
+ return null;
42
+ return new Date(ms);
43
+ };
44
+ /** Today, normalized to UTC midnight, as a Date. */
45
+ const todayUtc = () => {
46
+ const d = new Date();
47
+ d.setUTCHours(0, 0, 0, 0);
48
+ return d;
49
+ };
50
+ const daysBetween = (a, b) => {
51
+ const ms = b.getTime() - a.getTime();
52
+ return Math.round(ms / (24 * 60 * 60 * 1000));
53
+ };
54
+ /**
55
+ * Run all post-schema validation checks against a `search_stays` request.
56
+ *
57
+ * Returns null if everything passes; otherwise returns the model-facing
58
+ * error message string. Tools call this in `execute` *before* hitting
59
+ * the HTTP layer so we never burn an adapter call on a request the
60
+ * adapter would also reject.
61
+ */
62
+ export function validateSearchStaysRequest(req, now = todayUtc()) {
63
+ // -----------------------------------------------------------------------
64
+ // Date format
65
+ // -----------------------------------------------------------------------
66
+ const checkIn = parseYmdUtc(req.check_in);
67
+ if (!checkIn) {
68
+ return (`check_in is not a valid YYYY-MM-DD date: "${req.check_in}". ` +
69
+ 'Ask the user for the date in plain English and re-format it as YYYY-MM-DD.');
70
+ }
71
+ const checkOut = parseYmdUtc(req.check_out);
72
+ if (!checkOut) {
73
+ return (`check_out is not a valid YYYY-MM-DD date: "${req.check_out}". ` +
74
+ 'Ask the user for the date in plain English and re-format it as YYYY-MM-DD.');
75
+ }
76
+ // -----------------------------------------------------------------------
77
+ // Date order — by far the most common LLM mistake
78
+ // -----------------------------------------------------------------------
79
+ if (checkOut.getTime() <= checkIn.getTime()) {
80
+ return (`check_out (${req.check_out}) must be after check_in (${req.check_in}). ` +
81
+ 'Did you swap the dates? Re-check with the user and call search_stays again.');
82
+ }
83
+ // -----------------------------------------------------------------------
84
+ // check_in not in the past
85
+ // -----------------------------------------------------------------------
86
+ if (checkIn.getTime() < now.getTime()) {
87
+ return (`check_in (${req.check_in}) is in the past. ` +
88
+ 'Confirm the dates with the user — Expedia only sells future stays.');
89
+ }
90
+ // -----------------------------------------------------------------------
91
+ // Stay length cap
92
+ // -----------------------------------------------------------------------
93
+ const nights = daysBetween(checkIn, checkOut);
94
+ if (nights > MAX_NIGHTS) {
95
+ return (`Stay length is ${nights} nights, which exceeds the ${MAX_NIGHTS}-night maximum per search. ` +
96
+ 'Split the trip into multiple ≤30-night searches and combine the results yourself.');
97
+ }
98
+ // -----------------------------------------------------------------------
99
+ // check_in window cap (Expedia only quotes ~500 days out)
100
+ // -----------------------------------------------------------------------
101
+ const daysOut = daysBetween(now, checkIn);
102
+ if (daysOut > MAX_DAYS_OUT) {
103
+ return (`check_in (${req.check_in}) is ${daysOut} days from now, beyond the ${MAX_DAYS_OUT}-day booking window. ` +
104
+ 'Tell the user the destination doesn\'t have inventory open that far in the future yet, and ask if they want to try a date closer to now.');
105
+ }
106
+ // -----------------------------------------------------------------------
107
+ // Filter sanity (only when both ends of a range are present)
108
+ // -----------------------------------------------------------------------
109
+ const filters = req.filters;
110
+ if (filters) {
111
+ if (typeof filters.min_star_rating === 'number' &&
112
+ typeof filters.max_star_rating === 'number' &&
113
+ filters.min_star_rating > filters.max_star_rating) {
114
+ return (`Star rating filter is inverted: min_star_rating ${filters.min_star_rating} > max_star_rating ${filters.max_star_rating}. ` +
115
+ 'Did you swap them? Re-call search_stays with the bounds in the right order.');
116
+ }
117
+ if (typeof filters.price_min === 'number' &&
118
+ typeof filters.price_max === 'number' &&
119
+ filters.price_min > filters.price_max) {
120
+ return (`Price filter is inverted: price_min ${filters.price_min} > price_max ${filters.price_max}. ` +
121
+ 'Did you swap them? Re-call search_stays with the bounds in the right order.');
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+ /**
127
+ * Trim and validate an email address with a deliberately-loose check.
128
+ *
129
+ * The plugin doesn't try to be a perfect RFC 5322 validator — that's the
130
+ * adapter's job (it uses the same `email-validator` package and runs the
131
+ * disposable-domain check). All we want here is to reject things that
132
+ * obviously aren't emails before burning an HTTP call.
133
+ *
134
+ * Returns null on success, or a model-facing error string on failure.
135
+ */
136
+ export function validateEmailLoose(raw) {
137
+ if (typeof raw !== 'string') {
138
+ return 'Email must be a string. Ask the user for their email address.';
139
+ }
140
+ const trimmed = raw.trim();
141
+ if (trimmed.length === 0) {
142
+ return 'Email is empty. Ask the user for their email address.';
143
+ }
144
+ if (trimmed.length > 254) {
145
+ return 'Email is unrealistically long. Ask the user to double-check what they typed.';
146
+ }
147
+ // One @, non-empty local and domain parts, domain has at least one dot,
148
+ // no whitespace anywhere. This matches the adapter's first-pass regex.
149
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
150
+ return (`"${trimmed}" doesn't look like a valid email address. ` +
151
+ 'Ask the user to double-check the spelling.');
152
+ }
153
+ return null;
154
+ }
155
+ /**
156
+ * Strip whitespace, dashes, dots, and any non-digit characters from a
157
+ * pasted code, then return the result if it's exactly 6 ASCII digits.
158
+ *
159
+ * Returns null when the cleaned input isn't 6 digits — the verify tool
160
+ * uses this to short-circuit "the user pasted a token by mistake" or
161
+ * "the user pasted a phrase" without burning an HTTP call.
162
+ *
163
+ * Defensive cleanup matters because email clients sometimes paste
164
+ * non-breaking spaces or thin spaces around copied text.
165
+ */
166
+ export function sanitizeAndValidateCode(raw) {
167
+ if (typeof raw !== 'string')
168
+ return null;
169
+ const cleaned = raw.replace(/[^0-9]/g, '');
170
+ if (cleaned.length !== 6)
171
+ return null;
172
+ return cleaned;
173
+ }
174
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ {
2
+ "id": "stayfinder-plugin",
3
+ "name": "StayFinder",
4
+ "description": "Live hotel and vacation rental search via the StayFinder service. Returns real-time pricing, availability, and Expedia booking redirect links — never scrape, never construct booking URLs from training data.",
5
+ "skills": [
6
+ "./skills"
7
+ ],
8
+ "contracts": {
9
+ "tools": [
10
+ "search_stays",
11
+ "stayfinder_signup",
12
+ "stayfinder_verify"
13
+ ]
14
+ },
15
+ "configSchema": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "adapter_url": {
20
+ "type": "string",
21
+ "description": "Base URL of the StayFinder service. Defaults to the public hosted endpoint at https://api.stayfinder.riverintel.com if omitted."
22
+ },
23
+ "default_pos_country": {
24
+ "type": "string",
25
+ "pattern": "^[A-Z]{2}$",
26
+ "description": "ISO-3166-1 alpha-2 country code for point-of-sale (affects currency and pricing display). Defaults to US."
27
+ },
28
+ "default_currency": {
29
+ "type": "string",
30
+ "pattern": "^[A-Z]{3}$",
31
+ "description": "ISO-4217 currency code. Defaults to the POS country's primary currency."
32
+ },
33
+ "request_timeout_ms": {
34
+ "type": "integer",
35
+ "minimum": 1000,
36
+ "maximum": 60000,
37
+ "description": "HTTP timeout for service calls. Defaults to 10000 (10s)."
38
+ }
39
+ }
40
+ }
41
+ }
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@riverintel/stayfinder-plugin",
3
+ "version": "0.2.0",
4
+ "description": "OpenClaw plugin for live hotel and vacation rental search via the StayFinder service. Returns real-time pricing, availability, and Expedia booking redirect links — no scraping, no browser automation.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "skills/",
14
+ "openclaw.plugin.json",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "prepublishOnly": "npm run build && npm test"
30
+ },
31
+ "keywords": [
32
+ "openclaw",
33
+ "openclaw-plugin",
34
+ "stayfinder",
35
+ "lodging",
36
+ "hotels",
37
+ "vacation-rental",
38
+ "travel",
39
+ "expedia",
40
+ "vrbo"
41
+ ],
42
+ "author": "RiverIntel",
43
+ "license": "Apache-2.0",
44
+ "homepage": "https://github.com/RiverIntel/stayfinder#readme",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/RiverIntel/stayfinder.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/RiverIntel/stayfinder/issues"
51
+ },
52
+ "engines": {
53
+ "node": ">=20"
54
+ },
55
+ "openclaw": {
56
+ "extensions": [
57
+ "./dist/index.js"
58
+ ],
59
+ "compat": {
60
+ "pluginApi": ">=2026.4.5"
61
+ },
62
+ "build": {
63
+ "openclawVersion": "2026.4.5"
64
+ },
65
+ "install": {
66
+ "npmSpec": "@riverintel/stayfinder-plugin",
67
+ "defaultChoice": "npm",
68
+ "minHostVersion": ">=2026.4.5"
69
+ },
70
+ "release": {
71
+ "publishToClawHub": true,
72
+ "publishToNpm": true
73
+ }
74
+ },
75
+ "peerDependencies": {
76
+ "openclaw": ">=2026.4.5"
77
+ },
78
+ "dependencies": {
79
+ "@sinclair/typebox": "^0.33.0"
80
+ },
81
+ "devDependencies": {
82
+ "@types/node": "^22.7.5",
83
+ "openclaw": "2026.4.5",
84
+ "typescript": "^5.6.3",
85
+ "vitest": "^4.1.3"
86
+ }
87
+ }