@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/.env.example +7 -0
- package/CHANGELOG.md +5 -0
- package/README.md +50 -0
- package/commands.json +1096 -0
- package/index.ts +72 -0
- package/openapi-client.ts +187 -0
- package/openapi-tools.ts +2079 -0
- package/package.json +168 -0
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
|
+
}
|