Lynx Skill

Prompts

Stateless 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).

Install

openclaw skills install lynx-skill

lynx-skill

Stateless Go CLI that replicates every tool from lynx-mcp-server as standalone commands — no daemon, no SSE, no Bearer token.

Quick Install

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/

Credentials

The binary reads these environment variables. All three are required:

VariableDescription
LYNX_USERNAMELynx username
LYNX_PASSWORDLynx password
LYNX_COMPANY_CODECompany code

Available Commands

#CommandDescription
1file-search-by-party-nameSearch files by customer last name
2file-search-by-file-referenceSearch files by Lynx file reference
3retrieve-itineraryGet detailed itinerary for a file
4retrieve-file-documentsGet documents for a transaction
5attachment-uploadUpload a file attachment from disk
6file-document-saveSave document at the file level
7transaction-document-saveSave document at the transaction level

All commands output JSON to stdout. Errors go to stderr. Exit code 0 = success, 1 = failure.


1. file-search-by-party-name

Search for files by the customer's last name.

lynx file-search-by-party-name --party-name=LASTNAME

Flags:

FlagTypeRequiredDescription
--party-name`stringyesCustomer 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

2. file-search-by-file-reference

Search for a file using its Lynx file reference (e.g. FTXXXXXXXXX).

lynx file-search-by-file-reference --file-reference=FTXXXXXXXXX

Flags:

FlagTypeRequiredDescription
--file-referencestringyesLynx 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

3. retrieve-itinerary

Get the full itinerary for a given file.

lynx retrieve-itinerary --file-identifier=ID

Flags:

FlagShortTypeRequiredDefaultDescription
--file-identifier-fstringyesNumeric file identifier (from search results)
--show-cancelled-cboolnofalseInclude 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

4. retrieve-file-documents

Retrieve all documents associated with a specific transaction within a file.

lynx retrieve-file-documents \
  --file-identifier=ID \
  --transaction-identifier=TXID

Flags:

FlagTypeRequiredDescription
--file-identifierstringyesNumeric file identifier
--transaction-identifierstringyesTransaction 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

5. attachment-upload

Upload a file (PDF, image, etc.) from disk and associate it with a document.

lynx attachment-upload \
  --identifier=FILEID \
  --file=/path/to/document.pdf

Flags:

FlagTypeRequiredDescription
--identifierstringyesUnique identifier for the attachment
--filestringyesPath 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

6. file-document-save

Save (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:

FlagTypeRequiredDescription
--file-identifierstringyesNumeric file identifier
--namestringyesDocument name
--contentstringyesDocument content (plain text or HTML)
--typestringyesDocument type code — see valid values below
--attachment-urlstringnoAttachment URL from attachment-upload

Valid --type values:

CodeDisplay Name
SUPPSupplier Communication
CLINTAgent Communication
GENGeneral
INVAccounting Communication
AFTERAfter Hours Phone Inquiry
EMAILEmail
FLIGHFlight Information
BOOKIMail Merge Document
PHONEPhone Conversation
CLCOMTravelling Client Communication

⚠️ Only these exact codes are accepted. Passing an invalid code will be rejected before the request is sent. The documentType field returned by retrieve-file-documents is 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

7. transaction-document-save

Save (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:

FlagTypeRequiredDescription
--file-identifierstringyesNumeric file identifier
--transaction-identifierstringyesTransaction identifier
--namestringyesDocument name
--contentstringyesDocument content (plain text or HTML)
--typestringyesDocument type code — see valid values in file-document-save
--attachment-urlstringnoAttachment 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

SAMPLES — Multi-Step Workflows

Sample 1: Find a customer file, inspect itinerary, save a note

# 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

Sample 2: Upload an invoice and attach it to a file

# 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

Sample 3: Retrieve documents, check content, save a new receipt

# 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

Sample 4: Full document management lifecycle

# 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

Troubleshooting

SymptomLikely Cause
authentication failedMissing or invalid LYNX_* env vars
LYNX_USERNAME is not setEnvironment variable not defined
invalid partyName argumentMissing --party-name flag
invalid file reference argumentMissing --file-reference flag
failed to parse responseGWT-RPC format changed or invalid identifier
unexpected response formatAttachment upload was rejected (check file size)
exit code 1, no stdoutError written to stderr; run lynx <command> 2>&1 to inspect

Quick debug recipes

# 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

Architecture

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

Lessons from Implementation (AP-17)

1. GWT-RPC Requires Two Parsers

The Lynx backend returns two GWT-RPC serialization formats. The parser auto-detects:

  • Old format (pre-mid 2025): Type strings like 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.
  • Lazy serialization (current): Type strings are resolved through index-based references. The first mapped string in the data array determines which format is active.

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).

2. .env Auto-Load (Convenience vs Security)

cmd/cmd.go:13-33 implements a lightweight .env loader — no external dependency. It reads .env from the working directory at startup:

  • Lines are split on =, whitespace-trimmed
  • Only sets env vars that are not already set (env vars take precedence)
  • Silently returns if .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.

3. No Retry/Backoff (Deferred)

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:

  • CLI is designed for agent use (one-shot commands), not high-throughput automation
  • If 429 or transient failures are observed in practice, add a simple retry wrapper in lynx/client.go

4. Attachment Upload — Consolidated Parsing

The 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:

  1. Strips SUCCESS: prefix
  2. Trims trailing colon and whitespace
  3. Validates the result starts with /

5. MCP Identifier Typo Corrected

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.

6. Test Coverage

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

Differences from lynx-mcp-server

  • Stateless: no background server, no SSE, no Bearer token
  • Attachment upload: reads files from disk directly (not base64)
  • Session: each command authenticates independently (fresh JSESSIONID)
  • Flag style: --kebab-case flags (CLI convention); MCP uses camelCase
  • Binary name: the CLI binary is lynx (vs the MCP server binary lynx-mcp-server)
  • Output: JSON to stdout, errors to stderr