Install
openclaw skills install lynx-skillStateless Go CLI for the Lynx Reservations (www.lynx-reservations.com) travel agency system. Use this skill whenever the user needs to search files, retrieve itineraries, upload attachments, or manage documents in the Lynx system. Commands map 1:1 to the lynx-mcp-server MCP tools but run as standalone CLI commands with no background server. The user will need valid Lynx credentials (LYNX_USERNAME, LYNX_PASSWORD, LYNX_COMPANY_CODE).
openclaw skills install lynx-skillStateless Go CLI that replicates every tool from lynx-mcp-server as standalone commands — no daemon, no SSE, no Bearer token.
cd lynx-travel-agent/lynx-skill
go build -o bin/lynx .
# optional: put it in PATH
cp bin/lynx ~/bin/lynx # or /usr/local/bin/
The binary reads these environment variables. All three are required:
| Variable | Description |
|---|---|
LYNX_USERNAME | Lynx username |
LYNX_PASSWORD | Lynx password |
LYNX_COMPANY_CODE | Company code |
| # | Command | Description |
|---|---|---|
| 1 | file-search-by-party-name | Search files by customer last name |
| 2 | file-search-by-file-reference | Search files by Lynx file reference |
| 3 | retrieve-itinerary | Get detailed itinerary for a file |
| 4 | retrieve-file-documents | Get documents for a transaction |
| 5 | attachment-upload | Upload a file attachment from disk |
| 6 | file-document-save | Save document at the file level |
| 7 | transaction-document-save | Save document at the transaction level |
All commands output JSON to stdout. Errors go to stderr. Exit code 0 = success, 1 = failure.
file-search-by-party-nameSearch for files by the customer's last name.
lynx file-search-by-party-name --party-name=LASTNAME
Flags:
| Flag | Type | Required | Description |
|---|---|---|---|
| --party-name` | string | yes | Customer last name to search for |
Output:
{
"count": 1,
"results": [
{
"companyCode": "XX",
"clientIdentifier": "12345",
"clientReference": "REF-ABC",
"currency": "EUR",
"fileIdentifier": "12345",
"fileReference": "FTXXXXXXXXX",
"partyName": "SMITH",
"status": "Active",
"travelDate": "2026-06-15"
}
]
}
Examples:
lynx file-search-by-party-name --party-name=Smith
lynx file-search-by-party-name --party-name=Smi
file-search-by-file-referenceSearch for a file using its Lynx file reference (e.g. FTXXXXXXXXX).
lynx file-search-by-file-reference --file-reference=FTXXXXXXXXX
Flags:
| Flag | Type | Required | Description |
|---|---|---|---|
--file-reference | string | yes | Lynx file reference |
Output: Same JSON structure as file-search-by-party-name (count + results array).
Example:
lynx file-search-by-file-reference --file-reference=FT16476987
retrieve-itineraryGet the full itinerary for a given file.
lynx retrieve-itinerary --file-identifier=ID
Flags:
| Flag | Short | Type | Required | Default | Description |
|---|---|---|---|---|---|
--file-identifier | -f | string | yes | — | Numeric file identifier (from search results) |
--show-cancelled | -c | bool | no | false | Include cancelled bookings in the results |
Output:
{
"type": "...",
"partyName": "SMITH",
"fileReference": "FTXXXXXXXXX",
"fileIdentifier": "12345",
"clientIdentifier": "12345",
"agentReference": "AGT001",
"itineraryCount": 2,
"itineraries": [
{
"voucherIdentifier": "V123",
"date": "15 Jun 2026",
"transactionIdentifier": "btx12345",
"supplier": "Hotel ABC",
"status": "Confirmed",
"confirmationNumber": "CONF-001",
"location": "Paris"
}
]
}
Examples:
# Active bookings only (default)
lynx retrieve-itinerary --file-identifier=16476987
# Include cancelled bookings
lynx retrieve-itinerary --file-identifier=16476987 --show-cancelled
retrieve-file-documentsRetrieve all documents associated with a specific transaction within a file.
lynx retrieve-file-documents \
--file-identifier=ID \
--transaction-identifier=TXID
Flags:
| Flag | Type | Required | Description |
|---|---|---|---|
--file-identifier | string | yes | Numeric file identifier |
--transaction-identifier | string | yes | Transaction identifier (from itinerary) |
Output:
{
"count": 1,
"results": [
{
"fileIdentifier": "12345",
"transactionIdentifier": "btx12345",
"documentIdentifier": "doc_001",
"documentName": "Invoice",
"documentType": "SUPP",
"content": "<span>Invoice details</span>",
"attachmentUrl": "/documents/file/f16476987/d20250709064401.pdf"
}
]
}
Example:
lynx retrieve-file-documents \
--file-identifier=16476987 \
--transaction-identifier=btx12345
attachment-uploadUpload a file (PDF, image, etc.) from disk and associate it with a document.
lynx attachment-upload \
--identifier=FILEID \
--file=/path/to/document.pdf
Flags:
| Flag | Type | Required | Description |
|---|---|---|---|
--identifier | string | yes | Unique identifier for the attachment |
--file | string | yes | Path to the file on disk |
Output:
{
"attachmentUrl": "/documents/file/f16476987/d20250709064401.pdf"
}
The returned attachmentUrl can be passed to file-document-save or transaction-document-save as --attachment-url.
Examples:
lynx attachment-upload --identifier=f16476987 --file=./documents/invoice.pdf
lynx attachment-upload --identifier=f16476987 --file=./documents/receipt.jpg
file-document-saveSave (create or update) a document at the file level.
lynx file-document-save \
--file-identifier=ID \
--name=NAME \
--content=CONTENT \
--type=TYPE \
[--attachment-url=URL]
Flags:
| Flag | Type | Required | Description |
|---|---|---|---|
--file-identifier | string | yes | Numeric file identifier |
--name | string | yes | Document name |
--content | string | yes | Document content (plain text or HTML) |
--type | string | yes | Document type code — see valid values below |
--attachment-url | string | no | Attachment URL from attachment-upload |
Valid --type values:
| Code | Display Name |
|---|---|
SUPP | Supplier Communication |
CLINT | Agent Communication |
GEN | General |
INV | Accounting Communication |
AFTER | After Hours Phone Inquiry |
EMAIL | |
FLIGH | Flight Information |
BOOKI | Mail Merge Document |
PHONE | Phone Conversation |
CLCOM | Travelling Client Communication |
⚠️ Only these exact codes are accepted. Passing an invalid code will be rejected before the request is sent. The
documentTypefield returned byretrieve-file-documentsis the internal code (e.g.SUPP), which can be passed directly to--type.
Output:
{
"status": "ok"
}
Examples:
lynx file-document-save \
--file-identifier=16476987 \
--name="Note" \
--content="Customer requested change of date" \
--type=SUPP
lynx file-document-save \
--file-identifier=16476987 \
--name="Invoice" \
--content="<span>Invoice #1234 — 1500.00 EUR</span>" \
--type=INV \
--attachment-url=/documents/file/f16476987/d20250709064401.pdf
transaction-document-saveSave (create or update) a document at the transaction level.
lynx transaction-document-save \
--file-identifier=ID \
--transaction-identifier=TXID \
--name=NAME \
--content=CONTENT \
--type=TYPE \
[--attachment-url=URL]
Flags:
| Flag | Type | Required | Description |
|---|---|---|---|
--file-identifier | string | yes | Numeric file identifier |
--transaction-identifier | string | yes | Transaction identifier |
--name | string | yes | Document name |
--content | string | yes | Document content (plain text or HTML) |
--type | string | yes | Document type code — see valid values in file-document-save |
--attachment-url | string | no | Attachment URL from attachment-upload |
Output:
{
"status": "ok"
}
Examples:
lynx transaction-document-save \
--file-identifier=16476987 \
--transaction-identifier=btx12345 \
--name="Voucher" \
--content="Confirmation voucher for hotel booking" \
--type=SUPP
lynx transaction-document-save \
--file-identifier=16476987 \
--transaction-identifier=btx12345 \
--name="Invoice" \
--content="<b>Paid: 500 EUR</b>" \
--type=INV \
--attachment-url=/documents/file/f16476987/d20250709064401.pdf
# Step 1 — Search by customer last name
lynx file-search-by-party-name --party-name=Smith
# Step 2 — From the results, grab the fileIdentifier (e.g. 16476987)
# and inspect the itinerary
lynx retrieve-itinerary --file-identifier=16476987
# Step 3 — From the itinerary, get a transactionIdentifier (e.g. btx12345)
# and save a note at transaction level
lynx transaction-document-save \
--file-identifier=16476987 \
--transaction-identifier=btx12345 \
--name="Note" \
--content="Customer requested date change" \
--type=SUPP
# Step 1 — Search file by reference
lynx file-search-by-file-reference --file-reference=FT16476987
# Step 2 — Upload the invoice PDF from disk
lynx attachment-upload \
--identifier=f16476987 \
--file=./invoices/invoice_1234.pdf
# Step 3 — Save the document at file level with the attachment URL
lynx file-document-save \
--file-identifier=16476987 \
--name="Invoice #1234" \
--content="<span>Invoice 1234 — 1500 EUR</span>" \
--type=INVOICE \
--attachment-url=/documents/file/f16476987/d20250709064401.pdf
# Step 1 — Retrieve all documents for a transaction
lynx retrieve-file-documents \
--file-identifier=16476987 \
--transaction-identifier=btx12345
# Step 2 — Pipe the JSON output through jq to inspect document names
lynx retrieve-file-documents \
--file-identifier=16476987 \
--transaction-identifier=btx12345 \
| jq '.results[].documentName'
# Step 3 — Upload a payment receipt
lynx attachment-upload \
--identifier=f16476987 \
--file=./receipts/payment_receipt.pdf
# Step 4 — Save the receipt at transaction level
lynx transaction-document-save \
--file-identifier=16476987 \
--transaction-identifier=btx12345 \
--name="Payment Receipt" \
--content="<b>Paid: 500 EUR on 01-Jun-2026</b>" \
--type=RECEIPT \
--attachment-url=/documents/file/f16476987/d20250709064401.pdf
# 1. Find the file
lynx file-search-by-party-name --party-name=Smith
# 2. Get the itinerary (find transaction identifiers)
lynx retrieve-itinerary --file-identifier=16476987
# 3. Upload an attachment
lynx attachment-upload \
--identifier=f16476987 \
--file=./invoice.pdf
# 4. Save the document at file level
lynx file-document-save \
--file-identifier=16476987 \
--name="Invoice" \
--content="<span>See attached PDF</span>" \
--type=SUPP \
--attachment-url=/documents/file/f16476987/d20250709064401.pdf
# 5. Verify the document was saved by retrieving all documents
lynx retrieve-file-documents \
--file-identifier=16476987 \
--transaction-identifier=btx12345
| Symptom | Likely Cause |
|---|---|
authentication failed | Missing or invalid LYNX_* env vars |
LYNX_USERNAME is not set | Environment variable not defined |
invalid partyName argument | Missing --party-name flag |
invalid file reference argument | Missing --file-reference flag |
failed to parse response | GWT-RPC format changed or invalid identifier |
unexpected response format | Attachment upload was rejected (check file size) |
exit code 1, no stdout | Error written to stderr; run lynx <command> 2>&1 to inspect |
# Capture stderr
lynx file-search-by-party-name --party-name=Smith 2>&1
# Pretty-print JSON
lynx file-search-by-party-name --party-name=Smith | jq
# Extract specific field
lynx file-search-by-party-name --party-name=Smith | jq '.results[].fileIdentifier'
# Check env vars are set
env | grep LYNX
lynx-skill/
├── main.go # Entry point — calls cmd.Run()
├── go.mod # Module: dodmcdund.cc/lynx-travel-agent/lynxskill (Go 1.23.10)
├── .gitignore # Ignores /bin/, /lynxskill, .env
├── SKILL.md # This file — OpenClaw skill definition
├── README.md # User-facing documentation
├── lynx_architecture.md # Full architecture document (decisions, comparisons)
│
├── cmd/ # CLI commands — one file per command + dispatcher
│ ├── cmd.go # Command registry, dispatcher, .env auto-loader, config
│ ├── file_search_by_party_name.go
│ ├── file_search_by_file_reference.go
│ ├── retrieve_itinerary.go
│ ├── retrieve_file_documents.go
│ ├── attachment_upload.go
│ ├── file_document_save.go
│ └── transaction_document_save.go
│
├── gwt/ # GWT-RPC protocol layer
│ ├── types.go # GWT type constants (class names)
│ ├── build.go # GWT-RPC body builders (one func per action)
│ ├── parse.go # GWT response parsers + response structs
│ └── parse_test.go # Parser tests (10-result FileSearchResponse)
│
└── lynx/ # HTTP client layer
├── auth.go # GWT-RPC login → JSESSIONID + http.Client with cookiejar
└── client.go # HTTP helpers: DoGWTRequest, DoMultipartRequest, GWT error parse
The Lynx backend returns two GWT-RPC serialization formats. The parser auto-detects:
com.lynxtraveltech.client.shared.model.FileSearchResponse/2457361185 appear directly in the data array. The parser walks backward from the end of the parsed array, mapping indices to values.The ParseFileSearchResponse function uses backward scanning: it iterates from the last element backward, identifies FileSearchResults type markers, and extracts 10 fields per result. This approach correctly handles multi-result responses (tested with 10 results in parse_test.go).
cmd/cmd.go:13-33 implements a lightweight .env loader — no external dependency. It reads .env from the working directory at startup:
=, whitespace-trimmed.env doesn't exist.gitignore already includes .env to prevent credential leaks. If .env exists with real credentials, it will be ignored by git. This resolves the "ACTION REQUIRED" item from the initial architecture.
The MCP server implements exponential backoff retry (0s → 5s → 10s → 30s → 30s, max 5 attempts) with RetryHTTPRequest. The CLI does not implement retry yet. Rationale:
lynx/client.goThe MCP server has duplicated parseResponseBody functions in pkg/tools/attachment_upload.go:155 and pkg/rest/attachment_upload.go:146. The CLI consolidates this into a single parseAttachmentResponse in cmd/attachment_upload.go:84-101.
The Lynx backend returns SUCCESS:/path/to/file: (colon-delimited). The parser:
SUCCESS: prefix/The MCP server parameter is named identifer (missing 'i', consistent throughout the codebase). The CLI parameter correctly uses --identifier. This is a breaking change from the MCP naming but fixes the original typo.
gwt/parse_test.go contains a parser test with a real GWT-RPC response for file-search-by-party-name ("BRAY" search yielding 10 results). Run tests:
cd lynx-travel-agent/lynx-skill && go test ./gwt/ -v
JSESSIONID)--kebab-case flags (CLI convention); MCP uses camelCaselynx (vs the MCP server binary lynx-mcp-server)