@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.
- package/LICENSE +201 -0
- package/README.md +94 -0
- package/dist/adapter-client.d.ts +68 -0
- package/dist/adapter-client.d.ts.map +1 -0
- package/dist/adapter-client.js +149 -0
- package/dist/adapter-client.js.map +1 -0
- package/dist/credential-store.d.ts +71 -0
- package/dist/credential-store.d.ts.map +1 -0
- package/dist/credential-store.js +143 -0
- package/dist/credential-store.js.map +1 -0
- package/dist/errors.d.ts +55 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +160 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-config.d.ts +37 -0
- package/dist/plugin-config.d.ts.map +1 -0
- package/dist/plugin-config.js +63 -0
- package/dist/plugin-config.js.map +1 -0
- package/dist/thumbnails.d.ts +31 -0
- package/dist/thumbnails.d.ts.map +1 -0
- package/dist/thumbnails.js +32 -0
- package/dist/thumbnails.js.map +1 -0
- package/dist/tool-result.d.ts +19 -0
- package/dist/tool-result.d.ts.map +1 -0
- package/dist/tool-result.js +18 -0
- package/dist/tool-result.js.map +1 -0
- package/dist/tools/search-stays.d.ts +45 -0
- package/dist/tools/search-stays.d.ts.map +1 -0
- package/dist/tools/search-stays.js +191 -0
- package/dist/tools/search-stays.js.map +1 -0
- package/dist/tools/stayfinder-signup.d.ts +38 -0
- package/dist/tools/stayfinder-signup.d.ts.map +1 -0
- package/dist/tools/stayfinder-signup.js +102 -0
- package/dist/tools/stayfinder-signup.js.map +1 -0
- package/dist/tools/stayfinder-verify.d.ts +26 -0
- package/dist/tools/stayfinder-verify.d.ts.map +1 -0
- package/dist/tools/stayfinder-verify.js +124 -0
- package/dist/tools/stayfinder-verify.js.map +1 -0
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +51 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +174 -0
- package/dist/validation.js.map +1 -0
- package/openclaw.plugin.json +41 -0
- package/package.json +87 -0
- 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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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,IAAI,CAAS;IACb,UAAU,CAAS;IACnB,iBAAiB,CAAU;IAC3B,iBAAiB,CAAU;IAC3B,SAAS,CAAiB;IAC1B,SAAS,CAAU;IACnB,OAAO,CAAU;IACjB,OAAO,CAA2B;IAE3C,YAAY,QAA8B,EAAE,UAAkB;QAC5D,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,iBAAiB,GAAG,QAAQ,CAAC,KAAK,CAAC,mBAAmB,CAAC;QAC5D,IAAI,CAAC,iBAAiB,GAAG,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC;QAC3D,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,UAAU,IAAI,IAAI,CAAC;QACnD,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC;QAC3C,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC;QACvC,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC;IACxC,CAAC;CACF;AAED,8EAA8E;AAC9E,kCAAkC;AAClC,8EAA8E;AAE9E,MAAM,kBAAkB,GAAG,CAAC,CAAqB,EAAU,EAAE;IAC3D,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AACxC,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAiB;IACnD,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,eAAe;QACf,KAAK,cAAc;YACjB,OAAO,CACL,2CAA2C;gBAC3C,+DAA+D;gBAC/D,iGAAiG,CAClG,CAAC;QAEJ,KAAK,eAAe;YAClB,OAAO,CACL,yFAAyF;gBACzF,0EAA0E;gBAC1E,0FAA0F;gBAC1F,qEAAqE,CACtE,CAAC;QAEJ,KAAK,kBAAkB;YACrB,OAAO,CACL,8CAA8C;gBAC9C,6GAA6G,CAC9G,CAAC;QAEJ,8BAA8B;QAC9B,KAAK,eAAe;YAClB,OAAO,CACL,iDAAiD;gBACjD,0DAA0D,CAC3D,CAAC;QAEJ,KAAK,kBAAkB;YACrB,OAAO,CACL,iFAAiF;gBACjF,yGAAyG,CAC1G,CAAC;QAEJ,KAAK,qBAAqB;YACxB,OAAO,CACL,4BAA4B;gBAC5B,sBAAsB,kBAAkB,CAAC,GAAG,CAAC,iBAAiB,CAAC,6CAA6C,CAC7G,CAAC;QAEJ,KAAK,cAAc,CAAC,CAAC,CAAC;YACpB,MAAM,SAAS,GAAG,GAAG,CAAC,iBAAiB,CAAC;YACxC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;gBACnD,OAAO,CACL,oCAAoC,SAAS,IAAI,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,SAAS;oBAClG,kFAAkF,CACnF,CAAC;YACJ,CAAC;YACD,qEAAqE;YACrE,yEAAyE;YACzE,sDAAsD;YACtD,OAAO,CACL,2BAA2B;gBAC3B,yEAAyE,CAC1E,CAAC;QACJ,CAAC;QAED,KAAK,cAAc;YACjB,OAAO,CACL,2DAA2D;gBAC3D,yEAAyE,CAC1E,CAAC;QAEJ,KAAK,wBAAwB;YAC3B,OAAO,CACL,2DAA2D;gBAC3D,yEAAyE,CAC1E,CAAC;QAEJ,kCAAkC;QAClC,KAAK,uBAAuB;YAC1B,OAAO,CACL,0CAA0C;gBAC1C,sBAAsB,kBAAkB,CAAC,GAAG,CAAC,iBAAiB,CAAC,YAAY;gBAC3E,qGAAqG,CACtG,CAAC;QAEJ,KAAK,uBAAuB;YAC1B,OAAO,CACL,4EAA4E;gBAC5E,sBAAsB,kBAAkB,CAAC,GAAG,CAAC,iBAAiB,CAAC,YAAY;gBAC3E,wDAAwD,CACzD,CAAC;QAEJ,KAAK,uBAAuB;YAC1B,OAAO,CACL,oCAAoC,GAAG,CAAC,OAAO,KAAK;gBACpD,0GAA0G,CAC3G,CAAC;QAEJ,KAAK,uBAAuB,CAAC,CAAC,CAAC;YAC7B,MAAM,UAAU,GAAI,GAAG,CAAC,OAAO,EAAE,UAAmD,IAAI,EAAE,CAAC;YAC3F,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1B,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9D,OAAO,oEAAoE,IAAI,EAAE,CAAC;YACpF,CAAC;YACD,OAAO,6EAA6E,CAAC;QACvF,CAAC;QAED,KAAK,wBAAwB,CAAC;QAC9B,KAAK,kBAAkB;YACrB,OAAO,CACL,4DAA4D;gBAC5D,wDAAwD;gBACxD,WAAW,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,SAAS,IAAI,aAAa,GAAG,CAC5D,CAAC;QAEJ,KAAK,sBAAsB;YACzB,OAAO,CACL,8EAA8E;gBAC9E,cAAc,GAAG,CAAC,iBAAiB,IAAI,EAAE,0BAA0B;gBACnE,2EAA2E,CAC5E,CAAC;QAEJ,KAAK,iBAAiB,CAAC;QACvB,KAAK,eAAe;YAClB,OAAO,CACL,8CAA8C,GAAG,CAAC,OAAO,IAAI;gBAC7D,6EAA6E,CAC9E,CAAC;QAEJ,KAAK,gBAAgB;YACnB,OAAO,CACL,qDAAqD;gBACrD,qDAAqD,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,SAAS,IAAI,aAAa,GAAG,CACtG,CAAC;QAEJ;YACE,qEAAqE;YACrE,iDAAiD;YACjD,OAAO,qBAAqB,GAAG,CAAC,OAAO,WAAW,GAAG,CAAC,IAAI,GAAG,CAAC;IAClE,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|