@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,143 @@
1
+ /**
2
+ * On-disk credential store for the StayFinder plugin.
3
+ *
4
+ * The plugin writes a single JSON file at ~/.openclaw/credentials/stayfinder.json
5
+ * (mode 0600 — readable only by the user) containing the API token and
6
+ * the small amount of metadata the plugin needs to drive re-auth without
7
+ * re-prompting the user for their email.
8
+ *
9
+ * The credential file shape is documented in types.ts (CredentialFile)
10
+ * and matches what stayfinder_verify writes after a successful exchange.
11
+ *
12
+ * Why a file (and not OpenClaw's credential API):
13
+ * The plugin manifest's `credentialPath` field is a READ-time hint, not
14
+ * a write API. There's no documented runtime API for plugins to write
15
+ * their own credentials back through OpenClaw. Owning the file directly
16
+ * at the documented path is the simplest forward-compatible answer:
17
+ * if a write API ever lands, we switch to it; until then, the file
18
+ * format we control is the contract.
19
+ *
20
+ * Read pattern:
21
+ * - search-stays.ts reads the file on every call (no in-process cache)
22
+ * so a fresh stayfinder_verify takes effect immediately
23
+ * - The file is small (~200 bytes) and the read is local; the cost
24
+ * is irrelevant
25
+ *
26
+ * Write pattern:
27
+ * - stayfinder-verify.ts writes the file once after a successful
28
+ * /v1/signup/verify response
29
+ * - Atomic write via "write to temp + rename" so a crash mid-write
30
+ * can't leave a half-written file the next read would choke on
31
+ * - mode 0600 enforced explicitly (Node's default umask honors this
32
+ * but we set it again at write time as defense in depth)
33
+ */
34
+ import { existsSync, mkdirSync } from 'node:fs';
35
+ import { chmod, readFile, rename, writeFile } from 'node:fs/promises';
36
+ import { homedir } from 'node:os';
37
+ import { dirname, join } from 'node:path';
38
+ // ---------------------------------------------------------------------------
39
+ // Path resolution
40
+ // ---------------------------------------------------------------------------
41
+ /**
42
+ * Resolve the credential file path.
43
+ *
44
+ * Honors the OPENCLAW_HOME environment variable if set (used by OpenClaw's
45
+ * test/dev profiles to isolate state). Falls back to ~/.openclaw.
46
+ *
47
+ * The credentials/ subdirectory and the file itself are created on demand
48
+ * by writeCredential. They're never assumed to exist at read time.
49
+ */
50
+ export function resolveCredentialPath(env = process.env) {
51
+ const home = env.OPENCLAW_HOME ?? join(homedir(), '.openclaw');
52
+ return join(home, 'credentials', 'stayfinder.json');
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Public API
56
+ // ---------------------------------------------------------------------------
57
+ /**
58
+ * Read the credential file. Returns null if it doesn't exist or if its
59
+ * contents don't parse as the expected shape.
60
+ *
61
+ * Critically: a missing or unreadable file returns null, NOT an error.
62
+ * "No credentials" is a normal state — the search_stays tool uses it as
63
+ * the trigger to surface `unauthorized` and walk the user through setup.
64
+ *
65
+ * We DO throw on a permission denied or other unexpected I/O error,
66
+ * because that's an environmental problem the user needs to know about
67
+ * and "silently treat as no creds" would be wrong (we'd loop them
68
+ * through signup forever without ever fixing the underlying issue).
69
+ */
70
+ export async function readCredential(env = process.env) {
71
+ const path = resolveCredentialPath(env);
72
+ let raw;
73
+ try {
74
+ raw = await readFile(path, { encoding: 'utf-8' });
75
+ }
76
+ catch (err) {
77
+ if (err.code === 'ENOENT')
78
+ return null;
79
+ throw new Error(`Failed to read credential file at ${path}: ${err.message}. ` +
80
+ 'Check file permissions and re-run signup if needed.');
81
+ }
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(raw);
85
+ }
86
+ catch {
87
+ // Corrupt file — return null and let the user re-run signup. We
88
+ // don't try to recover; a malformed credentials file is rare and
89
+ // signup is cheap.
90
+ return null;
91
+ }
92
+ if (!isValidCredentialFile(parsed))
93
+ return null;
94
+ return parsed;
95
+ }
96
+ /**
97
+ * Write the credential file atomically with mode 0600.
98
+ *
99
+ * Creates the credentials/ directory (mode 0700) if it doesn't exist.
100
+ * Writes to a temp file in the same directory, sets permissions, then
101
+ * renames into place — so a crash mid-write leaves either the old file
102
+ * untouched or the new file complete, never a half-written file.
103
+ *
104
+ * Throws on any I/O failure. The signup_verify tool catches it and
105
+ * surfaces a clean error to the model.
106
+ */
107
+ export async function writeCredential(credential, env = process.env) {
108
+ const path = resolveCredentialPath(env);
109
+ const dir = dirname(path);
110
+ // Create directory with 0700 (rwx user only) if missing.
111
+ if (!existsSync(dir)) {
112
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
113
+ }
114
+ const tempPath = `${path}.tmp.${process.pid}`;
115
+ const body = JSON.stringify(credential, null, 2) + '\n';
116
+ await writeFile(tempPath, body, { mode: 0o600, encoding: 'utf-8' });
117
+ // Re-chmod defensively in case the file already existed with looser perms.
118
+ await chmod(tempPath, 0o600);
119
+ await rename(tempPath, path);
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Validation helpers
123
+ // ---------------------------------------------------------------------------
124
+ /**
125
+ * Type guard for CredentialFile. Used by readCredential to reject malformed
126
+ * files. We check every required field; an old file from a previous version
127
+ * of the plugin that lacks newer fields will be rejected and the user will
128
+ * re-run signup. That's the right tradeoff: re-signup is cheap (one paste)
129
+ * and silently accepting partial data risks weird state.
130
+ */
131
+ function isValidCredentialFile(value) {
132
+ if (typeof value !== 'object' || value === null)
133
+ return false;
134
+ const v = value;
135
+ return (typeof v.api_token === 'string' &&
136
+ v.api_token.length > 0 &&
137
+ typeof v.saved_at === 'string' &&
138
+ typeof v.tenant_id === 'string' &&
139
+ typeof v.email === 'string' &&
140
+ (v.token_kind === 'ephemeral' || v.token_kind === 'persistent') &&
141
+ (v.expires_at === null || typeof v.expires_at === 'string'));
142
+ }
143
+ //# sourceMappingURL=credential-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-store.js","sourceRoot":"","sources":["../src/credential-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Adapter error mapping.
3
+ *
4
+ * The plugin's three tools all talk to the StayFinder service, which
5
+ * returns a structured error envelope (see types.ts AdapterErrorEnvelope)
6
+ * for any non-2xx response. We translate that envelope into:
7
+ *
8
+ * 1. An `AdapterError` exception that the tool's `execute` function
9
+ * throws — the OpenClaw runtime catches the throw and surfaces it
10
+ * as a tool error to the model.
11
+ *
12
+ * 2. A short, model-facing message string that explains what happened
13
+ * in plain English with an actionable next step. Models react much
14
+ * better to "your access expired — call stayfinder_signup with the
15
+ * cached email" than to "HTTP 401 token_expired".
16
+ *
17
+ * The message text is the most important thing in the file. It's what
18
+ * the agent reads when deciding what to tell the user, and it's where
19
+ * we encode the recovery flows for `unauthorized` / `token_expired` /
20
+ * `code_attempts_exceeded` etc.
21
+ */
22
+ import type { AdapterErrorEnvelope } from './types.js';
23
+ /**
24
+ * Thrown by adapter-client.ts when the StayFinder service returns a non-2xx
25
+ * response. The constructor pulls fields out of the standard error envelope.
26
+ *
27
+ * Tool `execute` functions catch this, format the message, and re-throw a
28
+ * new Error with the formatted text — the OpenClaw runtime then surfaces
29
+ * the throw to the model. The structured fields stay accessible via the
30
+ * `code`, `retryAfterSeconds`, etc. properties for tools that need to
31
+ * branch on them (e.g., search-stays.ts treats `token_expired` differently
32
+ * from `unauthorized`).
33
+ */
34
+ export declare class AdapterError extends Error {
35
+ readonly code: string;
36
+ readonly httpStatus: number;
37
+ readonly retryAfterSeconds?: number;
38
+ readonly attemptsRemaining?: number;
39
+ readonly expiresAt?: string | null;
40
+ readonly requestId?: string;
41
+ readonly traceId?: string;
42
+ readonly details?: Record<string, unknown>;
43
+ constructor(envelope: AdapterErrorEnvelope, httpStatus: number);
44
+ }
45
+ /**
46
+ * Format an AdapterError as a single-string message the model can read.
47
+ *
48
+ * Each branch is hand-tuned for what the agent should DO with the error,
49
+ * not just what went wrong. We tell the model the next concrete action
50
+ * ("call stayfinder_signup with the cached email", "ask the user for a
51
+ * different email", "wait N minutes and retry") rather than leaving it
52
+ * to figure out the recovery flow on its own.
53
+ */
54
+ export declare function formatErrorForModel(err: AdapterError): string;
55
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAMvD;;;;;;;;;;GAUG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAE/B,QAAQ,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM;CAY/D;AAWD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAuI7D"}
package/dist/errors.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Adapter error mapping.
3
+ *
4
+ * The plugin's three tools all talk to the StayFinder service, which
5
+ * returns a structured error envelope (see types.ts AdapterErrorEnvelope)
6
+ * for any non-2xx response. We translate that envelope into:
7
+ *
8
+ * 1. An `AdapterError` exception that the tool's `execute` function
9
+ * throws — the OpenClaw runtime catches the throw and surfaces it
10
+ * as a tool error to the model.
11
+ *
12
+ * 2. A short, model-facing message string that explains what happened
13
+ * in plain English with an actionable next step. Models react much
14
+ * better to "your access expired — call stayfinder_signup with the
15
+ * cached email" than to "HTTP 401 token_expired".
16
+ *
17
+ * The message text is the most important thing in the file. It's what
18
+ * the agent reads when deciding what to tell the user, and it's where
19
+ * we encode the recovery flows for `unauthorized` / `token_expired` /
20
+ * `code_attempts_exceeded` etc.
21
+ */
22
+ // ---------------------------------------------------------------------------
23
+ // Exception class
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Thrown by adapter-client.ts when the StayFinder service returns a non-2xx
27
+ * response. The constructor pulls fields out of the standard error envelope.
28
+ *
29
+ * Tool `execute` functions catch this, format the message, and re-throw a
30
+ * new Error with the formatted text — the OpenClaw runtime then surfaces
31
+ * the throw to the model. The structured fields stay accessible via the
32
+ * `code`, `retryAfterSeconds`, etc. properties for tools that need to
33
+ * branch on them (e.g., search-stays.ts treats `token_expired` differently
34
+ * from `unauthorized`).
35
+ */
36
+ export class AdapterError extends Error {
37
+ code;
38
+ httpStatus;
39
+ retryAfterSeconds;
40
+ attemptsRemaining;
41
+ expiresAt;
42
+ requestId;
43
+ traceId;
44
+ details;
45
+ constructor(envelope, httpStatus) {
46
+ super(envelope.error.message);
47
+ this.name = 'AdapterError';
48
+ this.code = envelope.error.code;
49
+ this.httpStatus = httpStatus;
50
+ this.retryAfterSeconds = envelope.error.retry_after_seconds;
51
+ this.attemptsRemaining = envelope.error.attempts_remaining;
52
+ this.expiresAt = envelope.error.expires_at ?? null;
53
+ this.requestId = envelope.error.request_id;
54
+ this.traceId = envelope.error.trace_id;
55
+ this.details = envelope.error.details;
56
+ }
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Model-facing message formatting
60
+ // ---------------------------------------------------------------------------
61
+ const minutesFromSeconds = (s) => {
62
+ if (s === undefined || !Number.isFinite(s) || s <= 0)
63
+ return 1;
64
+ return Math.max(1, Math.ceil(s / 60));
65
+ };
66
+ /**
67
+ * Format an AdapterError as a single-string message the model can read.
68
+ *
69
+ * Each branch is hand-tuned for what the agent should DO with the error,
70
+ * not just what went wrong. We tell the model the next concrete action
71
+ * ("call stayfinder_signup with the cached email", "ask the user for a
72
+ * different email", "wait N minutes and retry") rather than leaving it
73
+ * to figure out the recovery flow on its own.
74
+ */
75
+ export function formatErrorForModel(err) {
76
+ switch (err.code) {
77
+ // Auth + setup
78
+ case 'unauthorized':
79
+ return ('StayFinder access is not configured yet. ' +
80
+ 'Run the first-time setup flow: ask the user for their email, ' +
81
+ 'call stayfinder_signup with it, then call stayfinder_verify with the 6-digit code they receive.');
82
+ case 'token_expired':
83
+ return ('StayFinder access expired from inactivity (the token slides off after ~7 days unused). ' +
84
+ 'Re-run the signup flow: call stayfinder_signup with the user\'s email — ' +
85
+ 'do NOT ask the user for their email, it is available from the cached credential record. ' +
86
+ 'Then call stayfinder_verify with the new 6-digit code they receive.');
87
+ case 'tenant_suspended':
88
+ return ('This StayFinder account has been suspended. ' +
89
+ 'You\'ll need to contact the operator to find out why; this is not something the user can fix from the chat.');
90
+ // Signup + verify error codes
91
+ case 'invalid_email':
92
+ return ('That doesn\'t look like a valid email address. ' +
93
+ 'Ask the user to double-check the spelling and try again.');
94
+ case 'disposable_email':
95
+ return ('That email provider isn\'t accepted (it\'s on the disposable-email blocklist). ' +
96
+ 'Ask the user to use a different email address — gmail, fastmail, icloud, or any other regular provider.');
97
+ case 'signup_rate_limited':
98
+ return ('Too many signup attempts. ' +
99
+ `Try again in about ${minutesFromSeconds(err.retryAfterSeconds)} minutes, or use a different email address.`);
100
+ case 'code_invalid': {
101
+ const remaining = err.attemptsRemaining;
102
+ if (typeof remaining === 'number' && remaining > 0) {
103
+ return (`That code didn't match. You have ${remaining} ${remaining === 1 ? 'attempt' : 'attempts'} left. ` +
104
+ 'Ask the user to double-check the email — it\'s a 6-digit number from StayFinder.');
105
+ }
106
+ // No attempts_remaining means there was no active pending row at all
107
+ // (for example, the user already verified an earlier code, or signup was
108
+ // never called). Fall through to "request a new one".
109
+ return ('That code didn\'t match. ' +
110
+ 'Call stayfinder_signup again to send a fresh code to the user\'s email.');
111
+ }
112
+ case 'code_expired':
113
+ return ('That code expired (codes are only valid for 15 minutes). ' +
114
+ 'Call stayfinder_signup again to send a fresh code to the user\'s email.');
115
+ case 'code_attempts_exceeded':
116
+ return ('Too many wrong attempts on that code — it\'s now locked. ' +
117
+ 'Call stayfinder_signup again to send a fresh code to the user\'s email.');
118
+ // Search-stays operational errors
119
+ case 'tenant_quota_exceeded':
120
+ return (`StayFinder hourly search limit reached. ` +
121
+ `Try again in about ${minutesFromSeconds(err.retryAfterSeconds)} minutes. ` +
122
+ 'Tell the user honestly — do not fall back to web_search or browser; those won\'t give live pricing.');
123
+ case 'global_quota_exceeded':
124
+ return ('The shared StayFinder rate limit is exhausted across all users right now. ' +
125
+ `Try again in about ${minutesFromSeconds(err.retryAfterSeconds)} minutes. ` +
126
+ 'This is operator-side, not the user\'s personal quota.');
127
+ case 'destination_not_found':
128
+ return (`Couldn't find that destination: "${err.message}". ` +
129
+ 'Ask the user to be more specific (city + state, or neighborhood + city), or suggest a nearby major city.');
130
+ case 'destination_ambiguous': {
131
+ const candidates = err.details?.candidates ?? [];
132
+ if (candidates.length > 0) {
133
+ const list = candidates.map((c) => `- ${c.label}`).join('\n');
134
+ return `Multiple destinations match. Ask the user which one they meant:\n${list}`;
135
+ }
136
+ return 'That destination matched multiple places. Ask the user to be more specific.';
137
+ }
138
+ case 'expedia_upstream_error':
139
+ case 'upstream_timeout':
140
+ return ('The lodging service is having upstream trouble right now. ' +
141
+ 'Tell the user briefly and offer to retry in a minute. ' +
142
+ `(trace: ${err.traceId ?? err.requestId ?? 'no trace id'})`);
143
+ case 'upstream_unavailable':
144
+ return ('The lodging service is temporarily shedding load (circuit breaker is open). ' +
145
+ `Wait about ${err.retryAfterSeconds ?? 30} seconds and try again. ` +
146
+ 'Tell the user honestly — this is a brief outage, not a permanent failure.');
147
+ case 'invalid_request':
148
+ case 'missing_field':
149
+ return (`Search request was rejected by validation: ${err.message}. ` +
150
+ 'Re-read the error message; usually a date or filter problem. Fix and retry.');
151
+ case 'internal_error':
152
+ return ('An internal error happened in the lodging service. ' +
153
+ `Tell the user briefly and offer to retry. (trace: ${err.traceId ?? err.requestId ?? 'no trace id'})`);
154
+ default:
155
+ // Unknown code — surface the raw message + code so we have something
156
+ // to grep for if it shows up in a session trace.
157
+ return `StayFinder error: ${err.message} (code: ${err.code})`;
158
+ }
159
+ }
160
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ configSchema: import("openclaw/plugin-sdk/core").OpenClawPluginConfigSchema;
6
+ register: NonNullable<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition["register"]>;
7
+ } & Pick<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
8
+ export default _default;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;AAkBA,wBAWG"}
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * StayFinder OpenClaw plugin entry point.
3
+ *
4
+ * Registers three tools:
5
+ * - search_stays — live hotel and vacation rental search
6
+ * - stayfinder_signup — request a 6-digit email verification code
7
+ * - stayfinder_verify — exchange the code for a token, saved to disk
8
+ *
9
+ * The bundled SKILL.md in skills/lodging-search/ tells the model when
10
+ * and how to use these tools. The skill is loaded automatically by
11
+ * OpenClaw when the plugin is installed (declared in openclaw.plugin.json).
12
+ */
13
+ import { definePluginEntry } from 'openclaw/plugin-sdk/core';
14
+ import { createSearchStaysTool } from './tools/search-stays.js';
15
+ import { createStayFinderSignupTool } from './tools/stayfinder-signup.js';
16
+ import { createStayFinderVerifyTool } from './tools/stayfinder-verify.js';
17
+ export default definePluginEntry({
18
+ id: 'stayfinder',
19
+ name: 'StayFinder',
20
+ description: 'Live hotel and vacation rental search via the StayFinder service. ' +
21
+ 'Returns real-time pricing, availability, and booking redirect links.',
22
+ register(api) {
23
+ api.registerTool(createSearchStaysTool(api));
24
+ api.registerTool(createStayFinderSignupTool(api));
25
+ api.registerTool(createStayFinderVerifyTool(api));
26
+ },
27
+ });
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE1E,eAAe,iBAAiB,CAAC;IAC/B,EAAE,EAAE,YAAY;IAChB,IAAI,EAAE,YAAY;IAClB,WAAW,EACT,oEAAoE;QACpE,sEAAsE;IACxE,QAAQ,CAAC,GAAG;QACV,GAAG,CAAC,YAAY,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7C,GAAG,CAAC,YAAY,CAAC,0BAA0B,CAAC,GAAG,CAAC,CAAC,CAAC;QAClD,GAAG,CAAC,YAAY,CAAC,0BAA0B,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Safe reader for the plugin's config block.
3
+ *
4
+ * OpenClaw passes plugin config as `api.pluginConfig: Record<string, unknown>`,
5
+ * which means we have to defensively type-check every field. The shape we
6
+ * expect is described by the `configSchema` in `openclaw.plugin.json` and
7
+ * by the `StayFinderPluginConfig` type in types.ts, but the runtime value
8
+ * is whatever happens to be in the user's `~/.openclaw/openclaw.json` file
9
+ * — possibly typoed, possibly missing fields, possibly hand-edited.
10
+ *
11
+ * The defaults here are the ones a fresh install gets if the user puts
12
+ * the minimum into their config (just `enabled: true`, no `config` block
13
+ * at all). They mirror what the README documents.
14
+ */
15
+ import type { StayFinderPluginConfig } from './types.js';
16
+ /**
17
+ * The public hosted StayFinder service. Self-hosters can point at their
18
+ * own deployment by setting `adapter_url` in plugin config.
19
+ */
20
+ export declare const DEFAULT_ADAPTER_URL = "https://api.stayfinder.riverintel.com";
21
+ export declare const DEFAULT_POS_COUNTRY = "US";
22
+ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
23
+ /**
24
+ * Read the plugin config from `api.pluginConfig` and apply defaults.
25
+ *
26
+ * Always returns a complete StayFinderPluginConfig object — no field is
27
+ * undefined except `default_currency`, which is genuinely optional and
28
+ * the adapter handles its absence by falling back to the POS-country
29
+ * default. Pass-through of an unset field beats injecting a wrong default.
30
+ *
31
+ * Strips `https://` schema validation from `adapter_url`: in dev we want
32
+ * to allow `http://localhost:8080`, but the configSchema in
33
+ * openclaw.plugin.json keeps the `https://` requirement to keep production
34
+ * users honest. The plugin runtime accepts whatever the schema lets through.
35
+ */
36
+ export declare function readPluginConfig(pluginConfig: unknown | undefined): StayFinderPluginConfig;
37
+ //# sourceMappingURL=plugin-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-config.d.ts","sourceRoot":"","sources":["../src/plugin-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,0CAA0C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAkCjD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,OAAO,GAAG,SAAS,GAChC,sBAAsB,CAYxB"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Safe reader for the plugin's config block.
3
+ *
4
+ * OpenClaw passes plugin config as `api.pluginConfig: Record<string, unknown>`,
5
+ * which means we have to defensively type-check every field. The shape we
6
+ * expect is described by the `configSchema` in `openclaw.plugin.json` and
7
+ * by the `StayFinderPluginConfig` type in types.ts, but the runtime value
8
+ * is whatever happens to be in the user's `~/.openclaw/openclaw.json` file
9
+ * — possibly typoed, possibly missing fields, possibly hand-edited.
10
+ *
11
+ * The defaults here are the ones a fresh install gets if the user puts
12
+ * the minimum into their config (just `enabled: true`, no `config` block
13
+ * at all). They mirror what the README documents.
14
+ */
15
+ /**
16
+ * The public hosted StayFinder service. Self-hosters can point at their
17
+ * own deployment by setting `adapter_url` in plugin config.
18
+ */
19
+ export const DEFAULT_ADAPTER_URL = 'https://api.stayfinder.riverintel.com';
20
+ export const DEFAULT_POS_COUNTRY = 'US';
21
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
22
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ const stringField = (raw, key, fallback) => {
24
+ const value = raw[key];
25
+ if (typeof value === 'string' && value.length > 0)
26
+ return value;
27
+ return fallback;
28
+ };
29
+ const optionalString = (raw, key) => {
30
+ const value = raw[key];
31
+ if (typeof value === 'string' && value.length > 0)
32
+ return value;
33
+ return undefined;
34
+ };
35
+ const numberField = (raw, key, fallback) => {
36
+ const value = raw[key];
37
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
38
+ return value;
39
+ return fallback;
40
+ };
41
+ /**
42
+ * Read the plugin config from `api.pluginConfig` and apply defaults.
43
+ *
44
+ * Always returns a complete StayFinderPluginConfig object — no field is
45
+ * undefined except `default_currency`, which is genuinely optional and
46
+ * the adapter handles its absence by falling back to the POS-country
47
+ * default. Pass-through of an unset field beats injecting a wrong default.
48
+ *
49
+ * Strips `https://` schema validation from `adapter_url`: in dev we want
50
+ * to allow `http://localhost:8080`, but the configSchema in
51
+ * openclaw.plugin.json keeps the `https://` requirement to keep production
52
+ * users honest. The plugin runtime accepts whatever the schema lets through.
53
+ */
54
+ export function readPluginConfig(pluginConfig) {
55
+ const raw = isRecord(pluginConfig) ? pluginConfig : {};
56
+ return {
57
+ adapter_url: stringField(raw, 'adapter_url', DEFAULT_ADAPTER_URL),
58
+ default_pos_country: stringField(raw, 'default_pos_country', DEFAULT_POS_COUNTRY),
59
+ default_currency: optionalString(raw, 'default_currency'),
60
+ request_timeout_ms: numberField(raw, 'request_timeout_ms', DEFAULT_REQUEST_TIMEOUT_MS),
61
+ };
62
+ }
63
+ //# sourceMappingURL=plugin-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-config.js","sourceRoot":"","sources":["../src/plugin-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;GAGG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,uCAAuC,CAAC;AAC3E,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACxC,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAEjD,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CACpE,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAEvE,MAAM,WAAW,GAAG,CAClB,GAA4B,EAC5B,GAAW,EACX,QAAgB,EACR,EAAE;IACV,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAChE,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CACrB,GAA4B,EAC5B,GAAW,EACS,EAAE;IACtB,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAChE,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAClB,GAA4B,EAC5B,GAAW,EACX,QAAgB,EACR,EAAE;IACV,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACnF,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAC9B,YAAiC;IAEjC,MAAM,GAAG,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;IACvD,OAAO;QACL,WAAW,EAAE,WAAW,CAAC,GAAG,EAAE,aAAa,EAAE,mBAAmB,CAAC;QACjE,mBAAmB,EAAE,WAAW,CAAC,GAAG,EAAE,qBAAqB,EAAE,mBAAmB,CAAC;QACjF,gBAAgB,EAAE,cAAc,CAAC,GAAG,EAAE,kBAAkB,CAAC;QACzD,kBAAkB,EAAE,WAAW,CAC7B,GAAG,EACH,oBAAoB,EACpB,0BAA0B,CAC3B;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Thumbnail URL resolution upgrade for Expedia CDN images.
3
+ *
4
+ * The adapter returns thumbnail_url with the tiny (_t, 70x70) suffix from
5
+ * Expedia's image CDN (images.trvl-media.com). For richer presentation
6
+ * contexts (Notion canvases, web UIs, single-property views), the model
7
+ * benefits from a higher-resolution image.
8
+ *
9
+ * Expedia's CDN uses a letter-based suffix convention before the file
10
+ * extension:
11
+ * _t = tiny (70x70)
12
+ * _s = small
13
+ * _y = medium (~500x214, good for cards)
14
+ * _l = large
15
+ * _z = full-resolution
16
+ *
17
+ * This module upgrades _t to _y (medium) by default — large enough for
18
+ * card layouts, small enough to be reasonable in bandwidth. The suffix
19
+ * swap is a simple string replacement; the CDN handles the rest.
20
+ */
21
+ type ImageSuffix = '_t' | '_s' | '_y' | '_l' | '_z';
22
+ /**
23
+ * Upgrade an Expedia CDN thumbnail URL to a higher resolution.
24
+ *
25
+ * Returns the original URL unchanged if it doesn't match the expected
26
+ * suffix pattern — defensive against non-Expedia URLs or future format
27
+ * changes.
28
+ */
29
+ export declare function upgradeThumbnailUrl(url: string, targetSuffix?: ImageSuffix): string;
30
+ export {};
31
+ //# sourceMappingURL=thumbnails.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnails.d.ts","sourceRoot":"","sources":["../src/thumbnails.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,KAAK,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAIpD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,YAAY,GAAE,WAAkB,GAC/B,MAAM,CAER"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Thumbnail URL resolution upgrade for Expedia CDN images.
3
+ *
4
+ * The adapter returns thumbnail_url with the tiny (_t, 70x70) suffix from
5
+ * Expedia's image CDN (images.trvl-media.com). For richer presentation
6
+ * contexts (Notion canvases, web UIs, single-property views), the model
7
+ * benefits from a higher-resolution image.
8
+ *
9
+ * Expedia's CDN uses a letter-based suffix convention before the file
10
+ * extension:
11
+ * _t = tiny (70x70)
12
+ * _s = small
13
+ * _y = medium (~500x214, good for cards)
14
+ * _l = large
15
+ * _z = full-resolution
16
+ *
17
+ * This module upgrades _t to _y (medium) by default — large enough for
18
+ * card layouts, small enough to be reasonable in bandwidth. The suffix
19
+ * swap is a simple string replacement; the CDN handles the rest.
20
+ */
21
+ const SUFFIX_PATTERN = /_(t|s|y|l|z)\.(jpe?g|png|webp)$/i;
22
+ /**
23
+ * Upgrade an Expedia CDN thumbnail URL to a higher resolution.
24
+ *
25
+ * Returns the original URL unchanged if it doesn't match the expected
26
+ * suffix pattern — defensive against non-Expedia URLs or future format
27
+ * changes.
28
+ */
29
+ export function upgradeThumbnailUrl(url, targetSuffix = '_y') {
30
+ return url.replace(SUFFIX_PATTERN, (_match, _letter, ext) => `${targetSuffix}.${ext}`);
31
+ }
32
+ //# sourceMappingURL=thumbnails.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnails.js","sourceRoot":"","sources":["../src/thumbnails.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,cAAc,GAAG,kCAAkC,CAAC;AAE1D;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAW,EACX,eAA4B,IAAI;IAEhC,OAAO,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,YAAY,IAAI,GAAG,EAAE,CAAC,CAAC;AACzF,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Tiny helper that builds the AgentToolResult shape the OpenClaw runtime
3
+ * expects. Inlined here instead of importing from `openclaw/plugin-sdk/core`
4
+ * because the SDK's bundled JS re-exports through a hashed chunk file
5
+ * (`common-B7pbdYUb.js`) that doesn't resolve cleanly as a named import
6
+ * from `openclaw/plugin-sdk/core` at test time. The function is 5 lines;
7
+ * owning it avoids a fragile import.
8
+ */
9
+ export interface ToolTextContent {
10
+ type: 'text';
11
+ text: string;
12
+ }
13
+ export interface ToolResult<T = unknown> {
14
+ content: ToolTextContent[];
15
+ details: T;
16
+ }
17
+ export declare function toolTextResult<T>(text: string, details: T): ToolResult<T>;
18
+ export declare function toolJsonResult<T>(payload: T): ToolResult<T>;
19
+ //# sourceMappingURL=tool-result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-result.d.ts","sourceRoot":"","sources":["../src/tool-result.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAKzE;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAE3D"}