@liveport/agent-sdk 0.1.0 → 0.1.1

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/dist/index.d.mts CHANGED
@@ -63,6 +63,7 @@ declare class ApiError extends Error {
63
63
  declare class LivePortAgent {
64
64
  private config;
65
65
  private abortController;
66
+ private waitInProgress;
66
67
  constructor(config: LivePortAgentConfig);
67
68
  /**
68
69
  * Wait for a tunnel to become available
package/dist/index.d.ts CHANGED
@@ -63,6 +63,7 @@ declare class ApiError extends Error {
63
63
  declare class LivePortAgent {
64
64
  private config;
65
65
  private abortController;
66
+ private waitInProgress;
66
67
  constructor(config: LivePortAgentConfig);
67
68
  /**
68
69
  * Wait for a tunnel to become available
package/dist/index.js CHANGED
@@ -44,10 +44,14 @@ var ApiError = class extends Error {
44
44
  var LivePortAgent = class {
45
45
  config;
46
46
  abortController = null;
47
+ waitInProgress = false;
47
48
  constructor(config) {
48
49
  if (!config.key) {
49
50
  throw new Error("Bridge key is required");
50
51
  }
52
+ if (config.timeout !== void 0 && config.timeout <= 0) {
53
+ throw new Error("Timeout must be greater than 0");
54
+ }
51
55
  this.config = {
52
56
  key: config.key,
53
57
  apiUrl: config.apiUrl || "https://app.liveport.dev",
@@ -65,35 +69,54 @@ var LivePortAgent = class {
65
69
  * @throws ApiError if the API request fails
66
70
  */
67
71
  async waitForTunnel(options) {
72
+ if (this.waitInProgress) {
73
+ throw new Error(
74
+ "Wait already in progress. Call disconnect() first or wait for the current operation to complete."
75
+ );
76
+ }
68
77
  const timeout = options?.timeout ?? this.config.timeout;
69
78
  const pollInterval = options?.pollInterval ?? 1e3;
79
+ if (timeout <= 0) {
80
+ throw new Error("Timeout must be greater than 0");
81
+ }
82
+ if (pollInterval <= 0) {
83
+ throw new Error("Poll interval must be greater than 0");
84
+ }
85
+ if (pollInterval > timeout) {
86
+ throw new Error("Poll interval cannot exceed timeout");
87
+ }
88
+ this.waitInProgress = true;
70
89
  this.abortController = new AbortController();
71
90
  const startTime = Date.now();
72
- while (Date.now() - startTime < timeout) {
73
- try {
74
- const response = await this.makeRequest(
75
- `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,
76
- { signal: this.abortController.signal }
77
- );
78
- if (response.ok) {
79
- const data = await response.json();
80
- if (data.tunnel) {
81
- return this.parseTunnel(data.tunnel);
91
+ try {
92
+ while (Date.now() - startTime < timeout) {
93
+ try {
94
+ const response = await this.makeRequest(
95
+ `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,
96
+ { signal: this.abortController.signal }
97
+ );
98
+ if (response.ok) {
99
+ const data = await response.json();
100
+ if (data.tunnel) {
101
+ return this.parseTunnel(data.tunnel);
102
+ }
103
+ } else if (response.status === 408) {
104
+ } else {
105
+ const error = await response.json().catch(() => ({ code: "UNKNOWN", message: "Request failed" }));
106
+ throw new ApiError(response.status, error.code, error.message);
107
+ }
108
+ } catch (err) {
109
+ if (err instanceof ApiError) throw err;
110
+ if (err.name === "AbortError") {
111
+ throw new Error("Wait cancelled");
82
112
  }
83
- } else if (response.status === 408) {
84
- } else {
85
- const error = await response.json().catch(() => ({ code: "UNKNOWN", message: "Request failed" }));
86
- throw new ApiError(response.status, error.code, error.message);
87
- }
88
- } catch (err) {
89
- if (err instanceof ApiError) throw err;
90
- if (err.name === "AbortError") {
91
- throw new Error("Wait cancelled");
92
113
  }
114
+ await this.sleep(pollInterval);
93
115
  }
94
- await this.sleep(pollInterval);
116
+ throw new TunnelTimeoutError(timeout);
117
+ } finally {
118
+ this.waitInProgress = false;
95
119
  }
96
- throw new TunnelTimeoutError(timeout);
97
120
  }
98
121
  /**
99
122
  * List all active tunnels for this bridge key
@@ -139,13 +162,45 @@ var LivePortAgent = class {
139
162
  * Parse tunnel response into AgentTunnel
140
163
  */
141
164
  parseTunnel(data) {
165
+ const tunnelId = data.tunnelId || data.id;
166
+ const subdomain = data.subdomain;
167
+ const url = data.url;
168
+ const localPort = data.localPort;
169
+ const createdAt = data.createdAt;
170
+ const expiresAt = data.expiresAt;
171
+ if (!tunnelId || typeof tunnelId !== "string") {
172
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid tunnelId in response");
173
+ }
174
+ if (!subdomain || typeof subdomain !== "string") {
175
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid subdomain in response");
176
+ }
177
+ if (!url || typeof url !== "string") {
178
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid url in response");
179
+ }
180
+ if (typeof localPort !== "number" || localPort <= 0 || localPort > 65535) {
181
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid localPort in response");
182
+ }
183
+ if (!createdAt || typeof createdAt !== "string") {
184
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid createdAt in response");
185
+ }
186
+ if (!expiresAt || typeof expiresAt !== "string") {
187
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid expiresAt in response");
188
+ }
189
+ const createdAtDate = new Date(createdAt);
190
+ const expiresAtDate = new Date(expiresAt);
191
+ if (isNaN(createdAtDate.getTime())) {
192
+ throw new ApiError(500, "INVALID_RESPONSE", "Invalid createdAt date format");
193
+ }
194
+ if (isNaN(expiresAtDate.getTime())) {
195
+ throw new ApiError(500, "INVALID_RESPONSE", "Invalid expiresAt date format");
196
+ }
142
197
  return {
143
- tunnelId: data.tunnelId || data.id,
144
- subdomain: data.subdomain,
145
- url: data.url,
146
- localPort: data.localPort,
147
- createdAt: new Date(data.createdAt),
148
- expiresAt: new Date(data.expiresAt)
198
+ tunnelId,
199
+ subdomain,
200
+ url,
201
+ localPort,
202
+ createdAt: createdAtDate,
203
+ expiresAt: expiresAtDate
149
204
  };
150
205
  }
151
206
  /**
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * LivePort Agent SDK\n *\n * TypeScript SDK for AI agents to wait for and access localhost tunnels.\n */\n\nimport type { Tunnel } from \"@liveport/shared\";\n\nexport interface LivePortAgentConfig {\n /** Bridge key for authentication */\n key: string;\n /** API base URL (default: https://app.liveport.dev) */\n apiUrl?: string;\n /** Default timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\nexport interface WaitForTunnelOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n}\n\nexport interface AgentTunnel {\n tunnelId: string;\n subdomain: string;\n url: string;\n localPort: number;\n createdAt: Date;\n expiresAt: Date;\n}\n\n/** Error thrown when tunnel wait times out */\nexport class TunnelTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Tunnel not available within ${timeout}ms timeout`);\n this.name = \"TunnelTimeoutError\";\n }\n}\n\n/** Error thrown when API request fails */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n\n/**\n * LivePort Agent SDK\n *\n * Allows AI agents to wait for and access localhost tunnels.\n *\n * @example\n * ```typescript\n * import { LivePortAgent } from '@liveport/agent-sdk';\n *\n * const agent = new LivePortAgent({\n * key: process.env.LIVEPORT_BRIDGE_KEY!\n * });\n *\n * // Wait for a tunnel to become available\n * const tunnel = await agent.waitForTunnel({ timeout: 30000 });\n * console.log(`Testing at: ${tunnel.url}`);\n *\n * // Run your tests against tunnel.url\n *\n * // Clean up when done\n * await agent.disconnect();\n * ```\n */\nexport class LivePortAgent {\n private config: Required<LivePortAgentConfig>;\n private abortController: AbortController | null = null;\n\n constructor(config: LivePortAgentConfig) {\n if (!config.key) {\n throw new Error(\"Bridge key is required\");\n }\n\n this.config = {\n key: config.key,\n apiUrl: config.apiUrl || \"https://app.liveport.dev\",\n timeout: config.timeout || 30000,\n };\n }\n\n /**\n * Wait for a tunnel to become available\n *\n * Long-polls the API until a tunnel is ready or timeout is reached.\n *\n * @param options - Wait options\n * @returns The tunnel info once available\n * @throws TunnelTimeoutError if no tunnel becomes available within timeout\n * @throws ApiError if the API request fails\n */\n async waitForTunnel(options?: WaitForTunnelOptions): Promise<AgentTunnel> {\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n\n this.abortController = new AbortController();\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await this.makeRequest(\n `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,\n { signal: this.abortController.signal }\n );\n\n if (response.ok) {\n const data = await response.json() as { tunnel?: Record<string, unknown> };\n if (data.tunnel) {\n return this.parseTunnel(data.tunnel);\n }\n } else if (response.status === 408) {\n // Timeout from server, continue polling\n } else {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n if ((err as Error).name === \"AbortError\") {\n throw new Error(\"Wait cancelled\");\n }\n // Network error, wait and retry\n }\n\n // Wait before next poll\n await this.sleep(pollInterval);\n }\n\n throw new TunnelTimeoutError(timeout);\n }\n\n /**\n * List all active tunnels for this bridge key\n *\n * @returns Array of active tunnels\n * @throws ApiError if the API request fails\n */\n async listTunnels(): Promise<AgentTunnel[]> {\n const response = await this.makeRequest(\"/api/agent/tunnels\");\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n\n const data = await response.json() as { tunnels?: Record<string, unknown>[] };\n return (data.tunnels || []).map((t) => this.parseTunnel(t));\n }\n\n /**\n * Disconnect and clean up\n *\n * Cancels any pending waitForTunnel calls.\n */\n async disconnect(): Promise<void> {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n }\n\n /**\n * Make an authenticated API request\n */\n private async makeRequest(\n path: string,\n options: RequestInit = {}\n ): Promise<Response> {\n const url = `${this.config.apiUrl}${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Authorization\": `Bearer ${this.config.key}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n }\n\n /**\n * Parse tunnel response into AgentTunnel\n */\n private parseTunnel(data: Record<string, unknown>): AgentTunnel {\n return {\n tunnelId: data.tunnelId as string || data.id as string,\n subdomain: data.subdomain as string,\n url: data.url as string,\n localPort: data.localPort as number,\n createdAt: new Date(data.createdAt as string),\n expiresAt: new Date(data.expiresAt as string),\n };\n }\n\n /**\n * Sleep helper\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n// Re-export types\nexport type { Tunnel } from \"@liveport/shared\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * LivePort Agent SDK\n *\n * TypeScript SDK for AI agents to wait for and access localhost tunnels.\n */\n\nimport type { Tunnel } from \"@liveport/shared\";\n\nexport interface LivePortAgentConfig {\n /** Bridge key for authentication */\n key: string;\n /** API base URL (default: https://app.liveport.dev) */\n apiUrl?: string;\n /** Default timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\nexport interface WaitForTunnelOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n}\n\nexport interface AgentTunnel {\n tunnelId: string;\n subdomain: string;\n url: string;\n localPort: number;\n createdAt: Date;\n expiresAt: Date;\n}\n\n/** Error thrown when tunnel wait times out */\nexport class TunnelTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Tunnel not available within ${timeout}ms timeout`);\n this.name = \"TunnelTimeoutError\";\n }\n}\n\n/** Error thrown when API request fails */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n\n/**\n * LivePort Agent SDK\n *\n * Allows AI agents to wait for and access localhost tunnels.\n *\n * @example\n * ```typescript\n * import { LivePortAgent } from '@liveport/agent-sdk';\n *\n * const agent = new LivePortAgent({\n * key: process.env.LIVEPORT_BRIDGE_KEY!\n * });\n *\n * // Wait for a tunnel to become available\n * const tunnel = await agent.waitForTunnel({ timeout: 30000 });\n * console.log(`Testing at: ${tunnel.url}`);\n *\n * // Run your tests against tunnel.url\n *\n * // Clean up when done\n * await agent.disconnect();\n * ```\n */\nexport class LivePortAgent {\n private config: Required<LivePortAgentConfig>;\n private abortController: AbortController | null = null;\n private waitInProgress = false;\n\n constructor(config: LivePortAgentConfig) {\n if (!config.key) {\n throw new Error(\"Bridge key is required\");\n }\n\n // Validate timeout before applying default\n if (config.timeout !== undefined && config.timeout <= 0) {\n throw new Error(\"Timeout must be greater than 0\");\n }\n\n this.config = {\n key: config.key,\n apiUrl: config.apiUrl || \"https://app.liveport.dev\",\n timeout: config.timeout || 30000,\n };\n }\n\n /**\n * Wait for a tunnel to become available\n *\n * Long-polls the API until a tunnel is ready or timeout is reached.\n *\n * @param options - Wait options\n * @returns The tunnel info once available\n * @throws TunnelTimeoutError if no tunnel becomes available within timeout\n * @throws ApiError if the API request fails\n */\n async waitForTunnel(options?: WaitForTunnelOptions): Promise<AgentTunnel> {\n // Prevent concurrent calls\n if (this.waitInProgress) {\n throw new Error(\n \"Wait already in progress. Call disconnect() first or wait for the current operation to complete.\"\n );\n }\n\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n\n // Validate options\n if (timeout <= 0) {\n throw new Error(\"Timeout must be greater than 0\");\n }\n if (pollInterval <= 0) {\n throw new Error(\"Poll interval must be greater than 0\");\n }\n if (pollInterval > timeout) {\n throw new Error(\"Poll interval cannot exceed timeout\");\n }\n\n this.waitInProgress = true;\n this.abortController = new AbortController();\n const startTime = Date.now();\n\n try {\n while (Date.now() - startTime < timeout) {\n try {\n const response = await this.makeRequest(\n `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,\n { signal: this.abortController.signal }\n );\n\n if (response.ok) {\n const data = await response.json() as { tunnel?: Record<string, unknown> };\n if (data.tunnel) {\n return this.parseTunnel(data.tunnel);\n }\n } else if (response.status === 408) {\n // Timeout from server, continue polling\n } else {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n if ((err as Error).name === \"AbortError\") {\n throw new Error(\"Wait cancelled\");\n }\n // Network error, wait and retry\n }\n\n // Wait before next poll\n await this.sleep(pollInterval);\n }\n\n throw new TunnelTimeoutError(timeout);\n } finally {\n this.waitInProgress = false;\n }\n }\n\n /**\n * List all active tunnels for this bridge key\n *\n * @returns Array of active tunnels\n * @throws ApiError if the API request fails\n */\n async listTunnels(): Promise<AgentTunnel[]> {\n const response = await this.makeRequest(\"/api/agent/tunnels\");\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n\n const data = await response.json() as { tunnels?: Record<string, unknown>[] };\n return (data.tunnels || []).map((t) => this.parseTunnel(t));\n }\n\n /**\n * Disconnect and clean up\n *\n * Cancels any pending waitForTunnel calls.\n */\n async disconnect(): Promise<void> {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n }\n\n /**\n * Make an authenticated API request\n */\n private async makeRequest(\n path: string,\n options: RequestInit = {}\n ): Promise<Response> {\n const url = `${this.config.apiUrl}${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Authorization\": `Bearer ${this.config.key}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n }\n\n /**\n * Parse tunnel response into AgentTunnel\n */\n private parseTunnel(data: Record<string, unknown>): AgentTunnel {\n const tunnelId = (data.tunnelId || data.id) as string;\n const subdomain = data.subdomain as string;\n const url = data.url as string;\n const localPort = data.localPort as number;\n const createdAt = data.createdAt as string;\n const expiresAt = data.expiresAt as string;\n\n // Validate required fields\n if (!tunnelId || typeof tunnelId !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid tunnelId in response\");\n }\n if (!subdomain || typeof subdomain !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid subdomain in response\");\n }\n if (!url || typeof url !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid url in response\");\n }\n if (typeof localPort !== \"number\" || localPort <= 0 || localPort > 65535) {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid localPort in response\");\n }\n if (!createdAt || typeof createdAt !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid createdAt in response\");\n }\n if (!expiresAt || typeof expiresAt !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid expiresAt in response\");\n }\n\n const createdAtDate = new Date(createdAt);\n const expiresAtDate = new Date(expiresAt);\n\n // Validate dates are valid\n if (isNaN(createdAtDate.getTime())) {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Invalid createdAt date format\");\n }\n if (isNaN(expiresAtDate.getTime())) {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Invalid expiresAt date format\");\n }\n\n return {\n tunnelId,\n subdomain,\n url,\n localPort,\n createdAt: createdAtDate,\n expiresAt: expiresAtDate,\n };\n }\n\n /**\n * Sleep helper\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n// Re-export types\nexport type { Tunnel } from \"@liveport/shared\";\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -18,10 +18,14 @@ var ApiError = class extends Error {
18
18
  var LivePortAgent = class {
19
19
  config;
20
20
  abortController = null;
21
+ waitInProgress = false;
21
22
  constructor(config) {
22
23
  if (!config.key) {
23
24
  throw new Error("Bridge key is required");
24
25
  }
26
+ if (config.timeout !== void 0 && config.timeout <= 0) {
27
+ throw new Error("Timeout must be greater than 0");
28
+ }
25
29
  this.config = {
26
30
  key: config.key,
27
31
  apiUrl: config.apiUrl || "https://app.liveport.dev",
@@ -39,35 +43,54 @@ var LivePortAgent = class {
39
43
  * @throws ApiError if the API request fails
40
44
  */
41
45
  async waitForTunnel(options) {
46
+ if (this.waitInProgress) {
47
+ throw new Error(
48
+ "Wait already in progress. Call disconnect() first or wait for the current operation to complete."
49
+ );
50
+ }
42
51
  const timeout = options?.timeout ?? this.config.timeout;
43
52
  const pollInterval = options?.pollInterval ?? 1e3;
53
+ if (timeout <= 0) {
54
+ throw new Error("Timeout must be greater than 0");
55
+ }
56
+ if (pollInterval <= 0) {
57
+ throw new Error("Poll interval must be greater than 0");
58
+ }
59
+ if (pollInterval > timeout) {
60
+ throw new Error("Poll interval cannot exceed timeout");
61
+ }
62
+ this.waitInProgress = true;
44
63
  this.abortController = new AbortController();
45
64
  const startTime = Date.now();
46
- while (Date.now() - startTime < timeout) {
47
- try {
48
- const response = await this.makeRequest(
49
- `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,
50
- { signal: this.abortController.signal }
51
- );
52
- if (response.ok) {
53
- const data = await response.json();
54
- if (data.tunnel) {
55
- return this.parseTunnel(data.tunnel);
65
+ try {
66
+ while (Date.now() - startTime < timeout) {
67
+ try {
68
+ const response = await this.makeRequest(
69
+ `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,
70
+ { signal: this.abortController.signal }
71
+ );
72
+ if (response.ok) {
73
+ const data = await response.json();
74
+ if (data.tunnel) {
75
+ return this.parseTunnel(data.tunnel);
76
+ }
77
+ } else if (response.status === 408) {
78
+ } else {
79
+ const error = await response.json().catch(() => ({ code: "UNKNOWN", message: "Request failed" }));
80
+ throw new ApiError(response.status, error.code, error.message);
81
+ }
82
+ } catch (err) {
83
+ if (err instanceof ApiError) throw err;
84
+ if (err.name === "AbortError") {
85
+ throw new Error("Wait cancelled");
56
86
  }
57
- } else if (response.status === 408) {
58
- } else {
59
- const error = await response.json().catch(() => ({ code: "UNKNOWN", message: "Request failed" }));
60
- throw new ApiError(response.status, error.code, error.message);
61
- }
62
- } catch (err) {
63
- if (err instanceof ApiError) throw err;
64
- if (err.name === "AbortError") {
65
- throw new Error("Wait cancelled");
66
87
  }
88
+ await this.sleep(pollInterval);
67
89
  }
68
- await this.sleep(pollInterval);
90
+ throw new TunnelTimeoutError(timeout);
91
+ } finally {
92
+ this.waitInProgress = false;
69
93
  }
70
- throw new TunnelTimeoutError(timeout);
71
94
  }
72
95
  /**
73
96
  * List all active tunnels for this bridge key
@@ -113,13 +136,45 @@ var LivePortAgent = class {
113
136
  * Parse tunnel response into AgentTunnel
114
137
  */
115
138
  parseTunnel(data) {
139
+ const tunnelId = data.tunnelId || data.id;
140
+ const subdomain = data.subdomain;
141
+ const url = data.url;
142
+ const localPort = data.localPort;
143
+ const createdAt = data.createdAt;
144
+ const expiresAt = data.expiresAt;
145
+ if (!tunnelId || typeof tunnelId !== "string") {
146
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid tunnelId in response");
147
+ }
148
+ if (!subdomain || typeof subdomain !== "string") {
149
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid subdomain in response");
150
+ }
151
+ if (!url || typeof url !== "string") {
152
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid url in response");
153
+ }
154
+ if (typeof localPort !== "number" || localPort <= 0 || localPort > 65535) {
155
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid localPort in response");
156
+ }
157
+ if (!createdAt || typeof createdAt !== "string") {
158
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid createdAt in response");
159
+ }
160
+ if (!expiresAt || typeof expiresAt !== "string") {
161
+ throw new ApiError(500, "INVALID_RESPONSE", "Missing or invalid expiresAt in response");
162
+ }
163
+ const createdAtDate = new Date(createdAt);
164
+ const expiresAtDate = new Date(expiresAt);
165
+ if (isNaN(createdAtDate.getTime())) {
166
+ throw new ApiError(500, "INVALID_RESPONSE", "Invalid createdAt date format");
167
+ }
168
+ if (isNaN(expiresAtDate.getTime())) {
169
+ throw new ApiError(500, "INVALID_RESPONSE", "Invalid expiresAt date format");
170
+ }
116
171
  return {
117
- tunnelId: data.tunnelId || data.id,
118
- subdomain: data.subdomain,
119
- url: data.url,
120
- localPort: data.localPort,
121
- createdAt: new Date(data.createdAt),
122
- expiresAt: new Date(data.expiresAt)
172
+ tunnelId,
173
+ subdomain,
174
+ url,
175
+ localPort,
176
+ createdAt: createdAtDate,
177
+ expiresAt: expiresAtDate
123
178
  };
124
179
  }
125
180
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * LivePort Agent SDK\n *\n * TypeScript SDK for AI agents to wait for and access localhost tunnels.\n */\n\nimport type { Tunnel } from \"@liveport/shared\";\n\nexport interface LivePortAgentConfig {\n /** Bridge key for authentication */\n key: string;\n /** API base URL (default: https://app.liveport.dev) */\n apiUrl?: string;\n /** Default timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\nexport interface WaitForTunnelOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n}\n\nexport interface AgentTunnel {\n tunnelId: string;\n subdomain: string;\n url: string;\n localPort: number;\n createdAt: Date;\n expiresAt: Date;\n}\n\n/** Error thrown when tunnel wait times out */\nexport class TunnelTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Tunnel not available within ${timeout}ms timeout`);\n this.name = \"TunnelTimeoutError\";\n }\n}\n\n/** Error thrown when API request fails */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n\n/**\n * LivePort Agent SDK\n *\n * Allows AI agents to wait for and access localhost tunnels.\n *\n * @example\n * ```typescript\n * import { LivePortAgent } from '@liveport/agent-sdk';\n *\n * const agent = new LivePortAgent({\n * key: process.env.LIVEPORT_BRIDGE_KEY!\n * });\n *\n * // Wait for a tunnel to become available\n * const tunnel = await agent.waitForTunnel({ timeout: 30000 });\n * console.log(`Testing at: ${tunnel.url}`);\n *\n * // Run your tests against tunnel.url\n *\n * // Clean up when done\n * await agent.disconnect();\n * ```\n */\nexport class LivePortAgent {\n private config: Required<LivePortAgentConfig>;\n private abortController: AbortController | null = null;\n\n constructor(config: LivePortAgentConfig) {\n if (!config.key) {\n throw new Error(\"Bridge key is required\");\n }\n\n this.config = {\n key: config.key,\n apiUrl: config.apiUrl || \"https://app.liveport.dev\",\n timeout: config.timeout || 30000,\n };\n }\n\n /**\n * Wait for a tunnel to become available\n *\n * Long-polls the API until a tunnel is ready or timeout is reached.\n *\n * @param options - Wait options\n * @returns The tunnel info once available\n * @throws TunnelTimeoutError if no tunnel becomes available within timeout\n * @throws ApiError if the API request fails\n */\n async waitForTunnel(options?: WaitForTunnelOptions): Promise<AgentTunnel> {\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n\n this.abortController = new AbortController();\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n try {\n const response = await this.makeRequest(\n `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,\n { signal: this.abortController.signal }\n );\n\n if (response.ok) {\n const data = await response.json() as { tunnel?: Record<string, unknown> };\n if (data.tunnel) {\n return this.parseTunnel(data.tunnel);\n }\n } else if (response.status === 408) {\n // Timeout from server, continue polling\n } else {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n if ((err as Error).name === \"AbortError\") {\n throw new Error(\"Wait cancelled\");\n }\n // Network error, wait and retry\n }\n\n // Wait before next poll\n await this.sleep(pollInterval);\n }\n\n throw new TunnelTimeoutError(timeout);\n }\n\n /**\n * List all active tunnels for this bridge key\n *\n * @returns Array of active tunnels\n * @throws ApiError if the API request fails\n */\n async listTunnels(): Promise<AgentTunnel[]> {\n const response = await this.makeRequest(\"/api/agent/tunnels\");\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n\n const data = await response.json() as { tunnels?: Record<string, unknown>[] };\n return (data.tunnels || []).map((t) => this.parseTunnel(t));\n }\n\n /**\n * Disconnect and clean up\n *\n * Cancels any pending waitForTunnel calls.\n */\n async disconnect(): Promise<void> {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n }\n\n /**\n * Make an authenticated API request\n */\n private async makeRequest(\n path: string,\n options: RequestInit = {}\n ): Promise<Response> {\n const url = `${this.config.apiUrl}${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Authorization\": `Bearer ${this.config.key}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n }\n\n /**\n * Parse tunnel response into AgentTunnel\n */\n private parseTunnel(data: Record<string, unknown>): AgentTunnel {\n return {\n tunnelId: data.tunnelId as string || data.id as string,\n subdomain: data.subdomain as string,\n url: data.url as string,\n localPort: data.localPort as number,\n createdAt: new Date(data.createdAt as string),\n expiresAt: new Date(data.expiresAt as string),\n };\n }\n\n /**\n * Sleep helper\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n// Re-export types\nexport type { Tunnel } from \"@liveport/shared\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * LivePort Agent SDK\n *\n * TypeScript SDK for AI agents to wait for and access localhost tunnels.\n */\n\nimport type { Tunnel } from \"@liveport/shared\";\n\nexport interface LivePortAgentConfig {\n /** Bridge key for authentication */\n key: string;\n /** API base URL (default: https://app.liveport.dev) */\n apiUrl?: string;\n /** Default timeout in milliseconds (default: 30000) */\n timeout?: number;\n}\n\nexport interface WaitForTunnelOptions {\n /** Timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Poll interval in milliseconds (default: 1000) */\n pollInterval?: number;\n}\n\nexport interface AgentTunnel {\n tunnelId: string;\n subdomain: string;\n url: string;\n localPort: number;\n createdAt: Date;\n expiresAt: Date;\n}\n\n/** Error thrown when tunnel wait times out */\nexport class TunnelTimeoutError extends Error {\n constructor(timeout: number) {\n super(`Tunnel not available within ${timeout}ms timeout`);\n this.name = \"TunnelTimeoutError\";\n }\n}\n\n/** Error thrown when API request fails */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n\n constructor(statusCode: number, code: string, message: string) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n }\n}\n\n/**\n * LivePort Agent SDK\n *\n * Allows AI agents to wait for and access localhost tunnels.\n *\n * @example\n * ```typescript\n * import { LivePortAgent } from '@liveport/agent-sdk';\n *\n * const agent = new LivePortAgent({\n * key: process.env.LIVEPORT_BRIDGE_KEY!\n * });\n *\n * // Wait for a tunnel to become available\n * const tunnel = await agent.waitForTunnel({ timeout: 30000 });\n * console.log(`Testing at: ${tunnel.url}`);\n *\n * // Run your tests against tunnel.url\n *\n * // Clean up when done\n * await agent.disconnect();\n * ```\n */\nexport class LivePortAgent {\n private config: Required<LivePortAgentConfig>;\n private abortController: AbortController | null = null;\n private waitInProgress = false;\n\n constructor(config: LivePortAgentConfig) {\n if (!config.key) {\n throw new Error(\"Bridge key is required\");\n }\n\n // Validate timeout before applying default\n if (config.timeout !== undefined && config.timeout <= 0) {\n throw new Error(\"Timeout must be greater than 0\");\n }\n\n this.config = {\n key: config.key,\n apiUrl: config.apiUrl || \"https://app.liveport.dev\",\n timeout: config.timeout || 30000,\n };\n }\n\n /**\n * Wait for a tunnel to become available\n *\n * Long-polls the API until a tunnel is ready or timeout is reached.\n *\n * @param options - Wait options\n * @returns The tunnel info once available\n * @throws TunnelTimeoutError if no tunnel becomes available within timeout\n * @throws ApiError if the API request fails\n */\n async waitForTunnel(options?: WaitForTunnelOptions): Promise<AgentTunnel> {\n // Prevent concurrent calls\n if (this.waitInProgress) {\n throw new Error(\n \"Wait already in progress. Call disconnect() first or wait for the current operation to complete.\"\n );\n }\n\n const timeout = options?.timeout ?? this.config.timeout;\n const pollInterval = options?.pollInterval ?? 1000;\n\n // Validate options\n if (timeout <= 0) {\n throw new Error(\"Timeout must be greater than 0\");\n }\n if (pollInterval <= 0) {\n throw new Error(\"Poll interval must be greater than 0\");\n }\n if (pollInterval > timeout) {\n throw new Error(\"Poll interval cannot exceed timeout\");\n }\n\n this.waitInProgress = true;\n this.abortController = new AbortController();\n const startTime = Date.now();\n\n try {\n while (Date.now() - startTime < timeout) {\n try {\n const response = await this.makeRequest(\n `/api/agent/tunnels/wait?timeout=${Math.min(pollInterval * 5, timeout - (Date.now() - startTime))}`,\n { signal: this.abortController.signal }\n );\n\n if (response.ok) {\n const data = await response.json() as { tunnel?: Record<string, unknown> };\n if (data.tunnel) {\n return this.parseTunnel(data.tunnel);\n }\n } else if (response.status === 408) {\n // Timeout from server, continue polling\n } else {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n if ((err as Error).name === \"AbortError\") {\n throw new Error(\"Wait cancelled\");\n }\n // Network error, wait and retry\n }\n\n // Wait before next poll\n await this.sleep(pollInterval);\n }\n\n throw new TunnelTimeoutError(timeout);\n } finally {\n this.waitInProgress = false;\n }\n }\n\n /**\n * List all active tunnels for this bridge key\n *\n * @returns Array of active tunnels\n * @throws ApiError if the API request fails\n */\n async listTunnels(): Promise<AgentTunnel[]> {\n const response = await this.makeRequest(\"/api/agent/tunnels\");\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ code: \"UNKNOWN\", message: \"Request failed\" })) as { code: string; message: string };\n throw new ApiError(response.status, error.code, error.message);\n }\n\n const data = await response.json() as { tunnels?: Record<string, unknown>[] };\n return (data.tunnels || []).map((t) => this.parseTunnel(t));\n }\n\n /**\n * Disconnect and clean up\n *\n * Cancels any pending waitForTunnel calls.\n */\n async disconnect(): Promise<void> {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n }\n\n /**\n * Make an authenticated API request\n */\n private async makeRequest(\n path: string,\n options: RequestInit = {}\n ): Promise<Response> {\n const url = `${this.config.apiUrl}${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Authorization\": `Bearer ${this.config.key}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n }\n\n /**\n * Parse tunnel response into AgentTunnel\n */\n private parseTunnel(data: Record<string, unknown>): AgentTunnel {\n const tunnelId = (data.tunnelId || data.id) as string;\n const subdomain = data.subdomain as string;\n const url = data.url as string;\n const localPort = data.localPort as number;\n const createdAt = data.createdAt as string;\n const expiresAt = data.expiresAt as string;\n\n // Validate required fields\n if (!tunnelId || typeof tunnelId !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid tunnelId in response\");\n }\n if (!subdomain || typeof subdomain !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid subdomain in response\");\n }\n if (!url || typeof url !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid url in response\");\n }\n if (typeof localPort !== \"number\" || localPort <= 0 || localPort > 65535) {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid localPort in response\");\n }\n if (!createdAt || typeof createdAt !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid createdAt in response\");\n }\n if (!expiresAt || typeof expiresAt !== \"string\") {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Missing or invalid expiresAt in response\");\n }\n\n const createdAtDate = new Date(createdAt);\n const expiresAtDate = new Date(expiresAt);\n\n // Validate dates are valid\n if (isNaN(createdAtDate.getTime())) {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Invalid createdAt date format\");\n }\n if (isNaN(expiresAtDate.getTime())) {\n throw new ApiError(500, \"INVALID_RESPONSE\", \"Invalid expiresAt date format\");\n }\n\n return {\n tunnelId,\n subdomain,\n url,\n localPort,\n createdAt: createdAtDate,\n expiresAt: expiresAtDate,\n };\n }\n\n /**\n * Sleep helper\n */\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\n// Re-export types\nexport type { Tunnel } from \"@liveport/shared\";\n"],"mappings":"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","names":[]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Basic Usage Example
3
+ *
4
+ * This example shows the simplest way to use the LivePort Agent SDK
5
+ * to wait for a tunnel and access a local development server.
6
+ */
7
+
8
+ import { LivePortAgent } from "@liveport/agent-sdk";
9
+
10
+ async function main() {
11
+ // Create agent instance with your bridge key
12
+ const agent = new LivePortAgent({
13
+ key: process.env.LIVEPORT_KEY || "lpk_your_bridge_key_here",
14
+ });
15
+
16
+ console.log("Waiting for tunnel to become available...");
17
+
18
+ try {
19
+ // Wait for tunnel (default 30 second timeout)
20
+ const tunnel = await agent.waitForTunnel();
21
+
22
+ console.log("✓ Tunnel established!");
23
+ console.log(` URL: ${tunnel.url}`);
24
+ console.log(` Local Port: ${tunnel.localPort}`);
25
+ console.log(` Subdomain: ${tunnel.subdomain}`);
26
+
27
+ // Now you can make requests to the tunnel URL
28
+ const response = await fetch(`${tunnel.url}/api/health`);
29
+ const data = await response.json();
30
+
31
+ console.log("Health check response:", data);
32
+ } catch (error) {
33
+ console.error("Failed to get tunnel:", error);
34
+ } finally {
35
+ // Clean up
36
+ await agent.disconnect();
37
+ }
38
+ }
39
+
40
+ main();
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Testing Integration Example
3
+ *
4
+ * This example shows how to integrate LivePort Agent SDK
5
+ * into your automated test suite (Vitest, Jest, etc.)
6
+ */
7
+
8
+ import { LivePortAgent, TunnelTimeoutError } from "@liveport/agent-sdk";
9
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
10
+
11
+ describe("API Integration Tests", () => {
12
+ let agent: LivePortAgent;
13
+ let tunnelUrl: string;
14
+
15
+ beforeAll(async () => {
16
+ // Initialize agent
17
+ agent = new LivePortAgent({
18
+ key: process.env.LIVEPORT_KEY!,
19
+ timeout: 60000, // 60 second timeout
20
+ });
21
+
22
+ try {
23
+ // Wait for developer to start their local server
24
+ console.log("⏳ Waiting for tunnel...");
25
+ const tunnel = await agent.waitForTunnel({ timeout: 60000 });
26
+ tunnelUrl = tunnel.url;
27
+ console.log(`✓ Connected to ${tunnelUrl}`);
28
+ } catch (error) {
29
+ if (error instanceof TunnelTimeoutError) {
30
+ throw new Error(
31
+ "Tunnel not available. Make sure the developer has started " +
32
+ "'liveport connect <port>' before running tests."
33
+ );
34
+ }
35
+ throw error;
36
+ }
37
+ });
38
+
39
+ afterAll(async () => {
40
+ await agent?.disconnect();
41
+ });
42
+
43
+ it("should return healthy status", async () => {
44
+ const response = await fetch(`${tunnelUrl}/api/health`);
45
+ expect(response.ok).toBe(true);
46
+
47
+ const data = await response.json();
48
+ expect(data.status).toBe("healthy");
49
+ });
50
+
51
+ it("should list users", async () => {
52
+ const response = await fetch(`${tunnelUrl}/api/users`);
53
+ expect(response.ok).toBe(true);
54
+
55
+ const users = await response.json();
56
+ expect(Array.isArray(users)).toBe(true);
57
+ });
58
+
59
+ it("should create a new user", async () => {
60
+ const response = await fetch(`${tunnelUrl}/api/users`, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({
64
+ name: "Test User",
65
+ email: "test@example.com",
66
+ }),
67
+ });
68
+
69
+ expect(response.status).toBe(201);
70
+
71
+ const user = await response.json();
72
+ expect(user.name).toBe("Test User");
73
+ expect(user.email).toBe("test@example.com");
74
+ });
75
+ });
@@ -0,0 +1,175 @@
1
+ /**
2
+ * AI Agent Workflow Example
3
+ *
4
+ * This example demonstrates how an AI agent might use the SDK
5
+ * to interact with a developer's local application.
6
+ */
7
+
8
+ import { LivePortAgent, ApiError } from "@liveport/agent-sdk";
9
+
10
+ interface TestResult {
11
+ endpoint: string;
12
+ status: "pass" | "fail";
13
+ responseTime: number;
14
+ statusCode?: number;
15
+ error?: string;
16
+ }
17
+
18
+ class AIAgentTester {
19
+ private agent: LivePortAgent;
20
+ private results: TestResult[] = [];
21
+
22
+ constructor(bridgeKey: string) {
23
+ this.agent = new LivePortAgent({
24
+ key: bridgeKey,
25
+ timeout: 120000, // 2 minute timeout for tunnel
26
+ });
27
+ }
28
+
29
+ async run(): Promise<void> {
30
+ console.log("🤖 AI Agent Test Runner Starting...\n");
31
+
32
+ try {
33
+ // Step 1: Wait for tunnel
34
+ console.log("⏳ Waiting for developer to start tunnel...");
35
+ const tunnel = await this.agent.waitForTunnel({
36
+ timeout: 120000,
37
+ });
38
+
39
+ console.log(`✓ Tunnel established: ${tunnel.url}`);
40
+ console.log(` Port: ${tunnel.localPort}`);
41
+ console.log(` Expires: ${tunnel.expiresAt.toLocaleString()}\n`);
42
+
43
+ // Step 2: Discover available endpoints
44
+ console.log("🔍 Discovering API endpoints...");
45
+ const baseUrl = tunnel.url;
46
+
47
+ // Test common endpoints
48
+ const endpoints = [
49
+ "/",
50
+ "/api/health",
51
+ "/api/users",
52
+ "/api/products",
53
+ "/api/auth/login",
54
+ ];
55
+
56
+ // Step 3: Test each endpoint
57
+ for (const endpoint of endpoints) {
58
+ await this.testEndpoint(baseUrl, endpoint);
59
+ }
60
+
61
+ // Step 4: Report results
62
+ this.printResults();
63
+
64
+ // Step 5: Analyze and provide recommendations
65
+ this.analyzeResults();
66
+ } catch (error) {
67
+ if (error instanceof ApiError) {
68
+ console.error(`❌ API Error [${error.code}]: ${error.message}`);
69
+ } else {
70
+ console.error("❌ Error:", error);
71
+ }
72
+ } finally {
73
+ await this.agent.disconnect();
74
+ }
75
+ }
76
+
77
+ private async testEndpoint(
78
+ baseUrl: string,
79
+ endpoint: string
80
+ ): Promise<void> {
81
+ const start = Date.now();
82
+
83
+ try {
84
+ const response = await fetch(`${baseUrl}${endpoint}`, {
85
+ signal: AbortSignal.timeout(5000), // 5 second timeout per request
86
+ });
87
+
88
+ const responseTime = Date.now() - start;
89
+
90
+ this.results.push({
91
+ endpoint,
92
+ status: response.ok ? "pass" : "fail",
93
+ responseTime,
94
+ statusCode: response.status,
95
+ });
96
+
97
+ const emoji = response.ok ? "✓" : "✗";
98
+ console.log(
99
+ ` ${emoji} ${endpoint.padEnd(20)} [${response.status}] ${responseTime}ms`
100
+ );
101
+ } catch (error) {
102
+ const responseTime = Date.now() - start;
103
+
104
+ this.results.push({
105
+ endpoint,
106
+ status: "fail",
107
+ responseTime,
108
+ error: error instanceof Error ? error.message : "Unknown error",
109
+ });
110
+
111
+ console.log(` ✗ ${endpoint.padEnd(20)} [ERROR] ${error}`);
112
+ }
113
+ }
114
+
115
+ private printResults(): void {
116
+ console.log("\n📊 Test Results Summary");
117
+ console.log("=" .repeat(50));
118
+
119
+ const passed = this.results.filter((r) => r.status === "pass").length;
120
+ const failed = this.results.filter((r) => r.status === "fail").length;
121
+ const avgTime =
122
+ this.results.reduce((sum, r) => sum + r.responseTime, 0) /
123
+ this.results.length;
124
+
125
+ console.log(`Total Endpoints: ${this.results.length}`);
126
+ console.log(`Passed: ${passed}`);
127
+ console.log(`Failed: ${failed}`);
128
+ console.log(`Average Response Time: ${avgTime.toFixed(0)}ms`);
129
+ }
130
+
131
+ private analyzeResults(): void {
132
+ console.log("\n💡 AI Analysis & Recommendations");
133
+ console.log("=" .repeat(50));
134
+
135
+ const slowEndpoints = this.results.filter((r) => r.responseTime > 1000);
136
+ if (slowEndpoints.length > 0) {
137
+ console.log("\n⚠️ Slow Endpoints (>1000ms):");
138
+ slowEndpoints.forEach((r) => {
139
+ console.log(` - ${r.endpoint}: ${r.responseTime}ms`);
140
+ });
141
+ console.log(" 💡 Consider optimizing database queries or adding caching");
142
+ }
143
+
144
+ const failedEndpoints = this.results.filter((r) => r.status === "fail");
145
+ if (failedEndpoints.length > 0) {
146
+ console.log("\n❌ Failed Endpoints:");
147
+ failedEndpoints.forEach((r) => {
148
+ console.log(` - ${r.endpoint}: ${r.statusCode || r.error}`);
149
+ });
150
+ console.log(" 💡 Review error handling and endpoint implementations");
151
+ }
152
+
153
+ const avgTime =
154
+ this.results.reduce((sum, r) => sum + r.responseTime, 0) /
155
+ this.results.length;
156
+ if (avgTime < 100) {
157
+ console.log("\n✨ Great performance! All endpoints responding quickly.");
158
+ }
159
+ }
160
+ }
161
+
162
+ // Run the AI agent
163
+ async function main() {
164
+ const bridgeKey = process.env.LIVEPORT_KEY;
165
+
166
+ if (!bridgeKey) {
167
+ console.error("Error: LIVEPORT_KEY environment variable not set");
168
+ process.exit(1);
169
+ }
170
+
171
+ const tester = new AIAgentTester(bridgeKey);
172
+ await tester.run();
173
+ }
174
+
175
+ main();
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Multiple Tunnels Example
3
+ *
4
+ * This example shows how to work with multiple tunnels
5
+ * when testing microservices or multi-service architectures.
6
+ */
7
+
8
+ import { LivePortAgent } from "@liveport/agent-sdk";
9
+
10
+ async function main() {
11
+ const agent = new LivePortAgent({
12
+ key: process.env.LIVEPORT_KEY!,
13
+ });
14
+
15
+ console.log("🔍 Checking for available tunnels...\n");
16
+
17
+ try {
18
+ // List all active tunnels for this bridge key
19
+ const tunnels = await agent.listTunnels();
20
+
21
+ if (tunnels.length === 0) {
22
+ console.log("No tunnels available.");
23
+ console.log("Start one or more tunnels with:");
24
+ console.log(" liveport connect 3000 # API server");
25
+ console.log(" liveport connect 3001 # Web server");
26
+ console.log(" liveport connect 5432 # Database");
27
+ return;
28
+ }
29
+
30
+ console.log(`Found ${tunnels.length} active tunnel(s):\n`);
31
+
32
+ // Display tunnel information
33
+ for (const tunnel of tunnels) {
34
+ console.log(`📍 ${tunnel.subdomain}`);
35
+ console.log(` URL: ${tunnel.url}`);
36
+ console.log(` Local Port: ${tunnel.localPort}`);
37
+ console.log(` Created: ${tunnel.createdAt.toLocaleString()}`);
38
+ console.log(` Expires: ${tunnel.expiresAt.toLocaleString()}`);
39
+ console.log();
40
+ }
41
+
42
+ // Example: Test each tunnel
43
+ console.log("Testing each tunnel...\n");
44
+
45
+ for (const tunnel of tunnels) {
46
+ try {
47
+ const response = await fetch(tunnel.url, {
48
+ signal: AbortSignal.timeout(5000),
49
+ });
50
+
51
+ console.log(
52
+ `✓ ${tunnel.subdomain.padEnd(30)} [${response.status}] ${response.statusText}`
53
+ );
54
+ } catch (error) {
55
+ console.log(`✗ ${tunnel.subdomain.padEnd(30)} [ERROR] ${error}`);
56
+ }
57
+ }
58
+
59
+ // Example: Find specific service by port
60
+ const apiTunnel = tunnels.find((t) => t.localPort === 3000);
61
+ if (apiTunnel) {
62
+ console.log(`\n🎯 Found API server at ${apiTunnel.url}`);
63
+
64
+ // Run API-specific tests
65
+ const health = await fetch(`${apiTunnel.url}/health`);
66
+ console.log(` Health check: ${health.status}`);
67
+ }
68
+
69
+ const webTunnel = tunnels.find((t) => t.localPort === 3001);
70
+ if (webTunnel) {
71
+ console.log(`\n🎯 Found web server at ${webTunnel.url}`);
72
+
73
+ // Run web-specific tests
74
+ const homepage = await fetch(webTunnel.url);
75
+ console.log(` Homepage: ${homepage.status}`);
76
+ }
77
+ } catch (error) {
78
+ console.error("Error:", error);
79
+ } finally {
80
+ await agent.disconnect();
81
+ }
82
+ }
83
+
84
+ main();
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Error Handling Example
3
+ *
4
+ * This example demonstrates proper error handling patterns
5
+ * when using the LivePort Agent SDK.
6
+ */
7
+
8
+ import {
9
+ LivePortAgent,
10
+ TunnelTimeoutError,
11
+ ApiError,
12
+ } from "@liveport/agent-sdk";
13
+
14
+ async function robustTunnelAccess() {
15
+ const agent = new LivePortAgent({
16
+ key: process.env.LIVEPORT_KEY!,
17
+ timeout: 30000,
18
+ });
19
+
20
+ try {
21
+ console.log("⏳ Waiting for tunnel...");
22
+
23
+ const tunnel = await agent.waitForTunnel({
24
+ timeout: 30000,
25
+ pollInterval: 2000,
26
+ });
27
+
28
+ console.log(`✓ Connected to ${tunnel.url}`);
29
+
30
+ // Your application logic here
31
+ await runTests(tunnel.url);
32
+ } catch (error) {
33
+ // Handle specific error types
34
+ if (error instanceof TunnelTimeoutError) {
35
+ console.error("\n❌ Tunnel Timeout");
36
+ console.error(" No tunnel became available within the timeout period.");
37
+ console.error("\n💡 Make sure to:");
38
+ console.error(" 1. Start your local server (e.g., npm run dev)");
39
+ console.error(" 2. Run: liveport connect <port>");
40
+ console.error(" 3. Wait for tunnel URL to appear");
41
+ console.error(" 4. Then run this script again");
42
+ process.exit(1);
43
+ } else if (error instanceof ApiError) {
44
+ console.error(`\n❌ API Error [${error.code}]`);
45
+ console.error(` ${error.message}`);
46
+ console.error(` Status: ${error.statusCode}`);
47
+
48
+ // Handle specific error codes
49
+ switch (error.code) {
50
+ case "INVALID_KEY":
51
+ console.error("\n💡 Your bridge key is invalid or expired.");
52
+ console.error(" Get a new key at: https://liveport.dev/keys");
53
+ break;
54
+
55
+ case "EXPIRED_KEY":
56
+ console.error("\n💡 Your bridge key has expired.");
57
+ console.error(" Create a new key at: https://liveport.dev/keys");
58
+ break;
59
+
60
+ case "REVOKED_KEY":
61
+ console.error("\n💡 Your bridge key has been revoked.");
62
+ console.error(" Create a new key at: https://liveport.dev/keys");
63
+ break;
64
+
65
+ case "USAGE_LIMIT_EXCEEDED":
66
+ console.error("\n💡 Your bridge key has exceeded its usage limit.");
67
+ console.error(" Create a new key or increase the limit at:");
68
+ console.error(" https://liveport.dev/keys");
69
+ break;
70
+
71
+ case "RATE_LIMIT_EXCEEDED":
72
+ console.error("\n💡 Too many requests. Please wait and try again.");
73
+ break;
74
+
75
+ default:
76
+ console.error("\n💡 Please check your configuration and try again.");
77
+ }
78
+
79
+ process.exit(1);
80
+ } else if (error instanceof Error) {
81
+ console.error("\n❌ Unexpected Error");
82
+ console.error(` ${error.message}`);
83
+ console.error("\n💡 Please report this issue at:");
84
+ console.error(" https://github.com/dundas/liveport/issues");
85
+ process.exit(1);
86
+ } else {
87
+ console.error("\n❌ Unknown Error");
88
+ console.error(error);
89
+ process.exit(1);
90
+ }
91
+ } finally {
92
+ // Always clean up
93
+ await agent.disconnect();
94
+ }
95
+ }
96
+
97
+ async function runTests(tunnelUrl: string) {
98
+ console.log("\n🧪 Running tests...");
99
+
100
+ try {
101
+ // Example test with retry logic
102
+ const response = await fetchWithRetry(`${tunnelUrl}/api/health`, {
103
+ maxRetries: 3,
104
+ retryDelay: 1000,
105
+ });
106
+
107
+ if (!response.ok) {
108
+ throw new Error(`Health check failed: ${response.status}`);
109
+ }
110
+
111
+ console.log("✓ Health check passed");
112
+
113
+ // More tests...
114
+ console.log("✓ All tests passed!");
115
+ } catch (error) {
116
+ console.error("✗ Tests failed:", error);
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ async function fetchWithRetry(
122
+ url: string,
123
+ options: { maxRetries?: number; retryDelay?: number } = {}
124
+ ): Promise<Response> {
125
+ const { maxRetries = 3, retryDelay = 1000 } = options;
126
+
127
+ for (let i = 0; i <= maxRetries; i++) {
128
+ try {
129
+ const response = await fetch(url, {
130
+ signal: AbortSignal.timeout(5000),
131
+ });
132
+
133
+ return response;
134
+ } catch (error) {
135
+ if (i === maxRetries) {
136
+ throw error;
137
+ }
138
+
139
+ console.log(` Retry ${i + 1}/${maxRetries} after ${retryDelay}ms...`);
140
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
141
+ }
142
+ }
143
+
144
+ throw new Error("Max retries exceeded");
145
+ }
146
+
147
+ // Run with proper error handling
148
+ robustTunnelAccess();
@@ -0,0 +1,172 @@
1
+ # LivePort Agent SDK Examples
2
+
3
+ This directory contains practical examples demonstrating different use cases for the LivePort Agent SDK.
4
+
5
+ ## Running the Examples
6
+
7
+ 1. **Install dependencies:**
8
+ ```bash
9
+ pnpm install
10
+ ```
11
+
12
+ 2. **Set your bridge key:**
13
+ ```bash
14
+ export LIVEPORT_KEY=lpk_your_bridge_key_here
15
+ ```
16
+
17
+ 3. **Start a local tunnel:**
18
+ ```bash
19
+ # In a separate terminal
20
+ liveport connect 3000
21
+ ```
22
+
23
+ 4. **Run an example:**
24
+ ```bash
25
+ npx tsx examples/01-basic-usage.ts
26
+ ```
27
+
28
+ ## Examples Overview
29
+
30
+ ### `01-basic-usage.ts`
31
+ **Simplest example** showing how to wait for a tunnel and make a request.
32
+
33
+ **Use case:** Quick start, proof of concept
34
+
35
+ **Key concepts:**
36
+ - Creating an agent instance
37
+ - Waiting for tunnel
38
+ - Making HTTP requests
39
+ - Basic error handling
40
+
41
+ ---
42
+
43
+ ### `02-testing-integration.ts`
44
+ **Testing framework integration** using Vitest (works with Jest too).
45
+
46
+ **Use case:** Automated testing, CI/CD pipelines
47
+
48
+ **Key concepts:**
49
+ - beforeAll/afterAll hooks
50
+ - Multiple test cases
51
+ - Sharing tunnel across tests
52
+ - Timeout configuration
53
+
54
+ ---
55
+
56
+ ### `03-ai-agent-workflow.ts`
57
+ **Complete AI agent workflow** that discovers, tests, and analyzes endpoints.
58
+
59
+ **Use case:** AI-powered testing, endpoint discovery, performance analysis
60
+
61
+ **Key concepts:**
62
+ - Endpoint discovery
63
+ - Performance measurement
64
+ - Result analysis
65
+ - AI recommendations
66
+
67
+ ---
68
+
69
+ ### `04-multiple-tunnels.ts`
70
+ **Working with multiple tunnels** for microservices testing.
71
+
72
+ **Use case:** Multi-service architectures, microservices
73
+
74
+ **Key concepts:**
75
+ - Listing all tunnels
76
+ - Finding tunnels by port
77
+ - Testing multiple services
78
+ - Service-specific logic
79
+
80
+ ---
81
+
82
+ ### `05-error-handling.ts`
83
+ **Comprehensive error handling** patterns and best practices.
84
+
85
+ **Use case:** Production applications, robust error handling
86
+
87
+ **Key concepts:**
88
+ - Specific error types
89
+ - User-friendly error messages
90
+ - Retry logic
91
+ - Cleanup and recovery
92
+
93
+ ---
94
+
95
+ ## Common Patterns
96
+
97
+ ### Pattern 1: Wait with Timeout
98
+
99
+ ```typescript
100
+ const agent = new LivePortAgent({ key: "lpk_..." });
101
+
102
+ try {
103
+ const tunnel = await agent.waitForTunnel({ timeout: 60000 });
104
+ // Use tunnel.url
105
+ } catch (error) {
106
+ if (error instanceof TunnelTimeoutError) {
107
+ console.log("No tunnel available");
108
+ }
109
+ }
110
+ ```
111
+
112
+ ### Pattern 2: Test Suite Integration
113
+
114
+ ```typescript
115
+ let tunnelUrl: string;
116
+
117
+ beforeAll(async () => {
118
+ const agent = new LivePortAgent({ key: process.env.LIVEPORT_KEY! });
119
+ const tunnel = await agent.waitForTunnel();
120
+ tunnelUrl = tunnel.url;
121
+ });
122
+
123
+ test("API test", async () => {
124
+ const response = await fetch(`${tunnelUrl}/api/endpoint`);
125
+ // assertions...
126
+ });
127
+ ```
128
+
129
+ ### Pattern 3: Multiple Services
130
+
131
+ ```typescript
132
+ const agent = new LivePortAgent({ key: "lpk_..." });
133
+ const tunnels = await agent.listTunnels();
134
+
135
+ const apiTunnel = tunnels.find(t => t.localPort === 3000);
136
+ const webTunnel = tunnels.find(t => t.localPort === 3001);
137
+
138
+ // Test API
139
+ await fetch(`${apiTunnel.url}/health`);
140
+
141
+ // Test Web
142
+ await fetch(`${webTunnel.url}/`);
143
+ ```
144
+
145
+ ## Tips
146
+
147
+ 1. **Always set a timeout** - Use reasonable timeouts to avoid hanging indefinitely
148
+ 2. **Handle errors gracefully** - Provide helpful error messages for users
149
+ 3. **Clean up resources** - Always call `agent.disconnect()` in finally blocks
150
+ 4. **Use environment variables** - Never hardcode bridge keys
151
+ 5. **Add retry logic** - Network requests can fail, especially in CI/CD
152
+
153
+ ## Environment Variables
154
+
155
+ All examples support these environment variables:
156
+
157
+ - `LIVEPORT_KEY` - Your bridge key (required)
158
+ - `LIVEPORT_API_URL` - Custom API URL (optional)
159
+
160
+ ## Next Steps
161
+
162
+ After trying these examples, check out:
163
+
164
+ - [API Documentation](../README.md)
165
+ - [CLI Documentation](../../cli/README.md)
166
+ - [Dashboard](https://liveport.dev/dashboard)
167
+
168
+ ## Need Help?
169
+
170
+ - 📚 [Full Documentation](https://liveport.dev/docs)
171
+ - 🐛 [Report Issues](https://github.com/dundas/liveport/issues)
172
+ - 💬 [Community Support](https://liveport.dev/support)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveport/agent-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "TypeScript SDK for AI agents to access LivePort tunnels",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
+ "examples",
17
18
  "README.md"
18
19
  ],
19
20
  "scripts": {
@@ -44,7 +45,7 @@
44
45
  },
45
46
  "repository": {
46
47
  "type": "git",
47
- "url": "git+https://github.com/dundas/liveport.git",
48
+ "url": "git+https://github.com/dundas/liveport-private.git",
48
49
  "directory": "packages/agent-sdk"
49
50
  },
50
51
  "publishConfig": {