@openpets/quo 1.0.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/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { config as loadDotenv } from "dotenv"
2
+ import { z, createPlugin, type ToolDefinition, createLogger, loadEnv } from "openpets-sdk"
3
+ import openAPITools from "./openapi-tools"
4
+
5
+ loadDotenv()
6
+
7
+ const AUTH_ENV_VARS = ["QUO_API_KEY"]
8
+
9
+ export const QuoPlugin = async () => {
10
+ const logger = createLogger("quo")
11
+ const env = loadEnv("quo") as Record<string, string | undefined>
12
+ const requiresAuth = true
13
+
14
+ const missingAuthVars = AUTH_ENV_VARS.filter((name) => {
15
+ const value = env[name] ?? process.env[name]
16
+ return !value
17
+ })
18
+
19
+ const isConfigured = !requiresAuth || missingAuthVars.length === 0
20
+
21
+ const testConnectionTool: ToolDefinition = {
22
+ name: "quo-test-connection",
23
+ description: "Test Quo configuration and generated OpenAPI tool availability",
24
+ schema: z.object({}),
25
+ async execute() {
26
+ if (!isConfigured) {
27
+ return JSON.stringify({
28
+ success: false,
29
+ status: "not_configured",
30
+ message: "Quo plugin is not configured. Set the required authentication environment variables.",
31
+ details: {
32
+ missingVariables: missingAuthVars,
33
+ requiredVariables: AUTH_ENV_VARS
34
+ }
35
+ }, null, 2)
36
+ }
37
+
38
+ return JSON.stringify({
39
+ success: true,
40
+ status: "connected",
41
+ message: "Quo OpenAPI tools loaded successfully",
42
+ details: {
43
+ toolCount: generatedTools.length,
44
+ requiresAuth,
45
+ configuredAuthVariables: AUTH_ENV_VARS
46
+ }
47
+ }, null, 2)
48
+ }
49
+ }
50
+
51
+ const generatedTools = openAPITools.filter(tool => tool.name !== "quo-test-connection")
52
+
53
+ if (!isConfigured) {
54
+ logger.warn("Quo plugin not configured - returning test-connection tool only", {
55
+ missingAuthVars
56
+ })
57
+ return createPlugin([testConnectionTool])
58
+ }
59
+
60
+ logger.info("Quo plugin configured - loading generated OpenAPI tools", {
61
+ toolCount: generatedTools.length
62
+ })
63
+
64
+ const tools: ToolDefinition[] = [
65
+ testConnectionTool,
66
+ ...generatedTools
67
+ ]
68
+
69
+ return createPlugin(tools)
70
+ }
71
+
72
+ export default QuoPlugin
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Auto-generated OpenAPI client utilities
3
+ *
4
+ * Source: https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json
5
+ * API: OpenPhone Public API v1.0.0
6
+ *
7
+ * This file contains reusable client utilities (getBaseUrl, getAuthHeaders, fetchAPI)
8
+ * that can be imported by custom tools or the generated openapi-tools.ts file.
9
+ *
10
+ * DO NOT EDIT MANUALLY - Regenerate with: pets generate-openapi
11
+ */
12
+
13
+ // Auth env var: QUO_API_KEY
14
+
15
+ export function getBaseUrl(): string {
16
+ const host = process.env.QUO_HOST || "https://api.openphone.com"
17
+ return host.endsWith('/') ? host.slice(0, -1) : host
18
+ }
19
+
20
+ function formatApiKey(apiKey: string): string {
21
+ const prefix = ""
22
+ return prefix && !apiKey.startsWith(prefix) ? `${prefix}${apiKey}` : apiKey
23
+ }
24
+
25
+
26
+
27
+ export function getAuthHeaders(): Record<string, string> {
28
+ const headers: Record<string, string> = {
29
+ "Content-Type": "application/json",
30
+ "Accept": "application/json",
31
+ }
32
+
33
+ const apiKey = process.env.QUO_API_KEY
34
+ if (apiKey) {
35
+ headers["Authorization"] = formatApiKey(apiKey)
36
+ }
37
+
38
+ return headers
39
+ }
40
+
41
+
42
+ /**
43
+ * Convert JSON to TOON format (optimized for LLM consumption)
44
+ * TOON format: https://github.com/toon-format/toon
45
+ * This function is also exported from openpets-sdk as jsonToToon
46
+ */
47
+ function jsonToToon(data: any, indent: string = ""): string {
48
+ if (data === null) return "null"
49
+ if (data === undefined) return "undefined"
50
+
51
+ const type = typeof data
52
+
53
+ if (type === "string") return data
54
+ if (type === "number" || type === "boolean") return String(data)
55
+
56
+ if (Array.isArray(data)) {
57
+ if (data.length === 0) return "[]"
58
+
59
+ const items = data.map((item, index) => {
60
+ const converted = jsonToToon(item, indent + " ")
61
+ if (typeof item === "object" && item !== null) {
62
+ return `${indent}[${index}]\n${converted}`
63
+ }
64
+ return `${indent}[${index}] ${converted}`
65
+ })
66
+
67
+ return items.join("\n")
68
+ }
69
+
70
+ if (type === "object") {
71
+ const entries = Object.entries(data)
72
+ if (entries.length === 0) return "{}"
73
+
74
+ const lines = entries.map(([key, value]) => {
75
+ const converted = jsonToToon(value, indent + " ")
76
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
77
+ return `${indent}${key}:\n${converted}`
78
+ } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object") {
79
+ return `${indent}${key}:\n${converted}`
80
+ }
81
+ return `${indent}${key}: ${converted}`
82
+ })
83
+
84
+ return lines.join("\n")
85
+ }
86
+
87
+ return String(data)
88
+ }
89
+
90
+
91
+ type DebugContext = {
92
+ debug?: boolean
93
+ debugLog?: (stage: string, details?: Record<string, any>) => void
94
+ }
95
+
96
+ function redactHeaders(headers: Record<string, string>): Record<string, string> {
97
+ const redacted: Record<string, string> = {}
98
+ for (const [key, value] of Object.entries(headers)) {
99
+ redacted[key] = /authorization|api[-_]?key|token|secret/i.test(key)
100
+ ? "[redacted]"
101
+ : value
102
+ }
103
+ return redacted
104
+ }
105
+
106
+ function previewBody(body: string, maxChars: number = 20000): string {
107
+ if (body.length <= maxChars) return body
108
+ return `${body.slice(0, maxChars)}... [truncated]`
109
+ }
110
+
111
+ function emitDebug(context: DebugContext | undefined, stage: string, details: Record<string, any>) {
112
+ if (context?.debug !== true) return
113
+ if (typeof context.debugLog === "function") {
114
+ context.debugLog(stage, details)
115
+ return
116
+ }
117
+ console.error(`[openapi debug] ${stage} ${JSON.stringify(details)}`)
118
+ }
119
+
120
+ export async function fetchAPI(url: string, options: RequestInit, context?: DebugContext): Promise<string> {
121
+ const responseFormat = "toon"
122
+ const startedAt = Date.now()
123
+ try {
124
+ const authHeaders = getAuthHeaders()
125
+ const mergedHeaders = { ...authHeaders, ...(options.headers as Record<string, string> || {}) }
126
+
127
+ emitDebug(context, "openapi api request", {
128
+ method: options.method || "GET",
129
+ url,
130
+ headers: redactHeaders(mergedHeaders),
131
+ body: typeof options.body === "string" ? previewBody(options.body) : undefined
132
+ })
133
+
134
+ const response = await fetch(url, { ...options, headers: mergedHeaders })
135
+ const contentType = response.headers.get("content-type")
136
+ const responseText = await response.text()
137
+
138
+ emitDebug(context, "openapi api response", {
139
+ method: options.method || "GET",
140
+ url,
141
+ status: response.status,
142
+ ok: response.ok,
143
+ durationMs: Date.now() - startedAt,
144
+ contentType,
145
+ body: previewBody(responseText)
146
+ })
147
+
148
+ if (!response.ok) {
149
+ const errorObj = {
150
+ success: false,
151
+ error: `HTTP ${response.status}: ${responseText}`,
152
+ status: response.status
153
+ }
154
+
155
+ if (responseFormat === "toon") {
156
+ return jsonToToon(errorObj)
157
+ }
158
+ return JSON.stringify(errorObj, null, 2)
159
+ }
160
+
161
+ if (contentType?.includes("application/json")) {
162
+ const data = responseText.trim().length > 0 ? JSON.parse(responseText) : null
163
+
164
+ if (responseFormat === "toon") {
165
+ return jsonToToon(data)
166
+ }
167
+ return JSON.stringify(data, null, 2)
168
+ }
169
+
170
+ const result = { success: true, data: responseText }
171
+
172
+ if (responseFormat === "toon") {
173
+ return jsonToToon(result)
174
+ }
175
+ return JSON.stringify(result, null, 2)
176
+ } catch (error: any) {
177
+ const errorObj = {
178
+ success: false,
179
+ error: error.message
180
+ }
181
+
182
+ if (responseFormat === "toon") {
183
+ return jsonToToon(errorObj)
184
+ }
185
+ return JSON.stringify(errorObj, null, 2)
186
+ }
187
+ }