epa-testeprojetoia 0.1.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/.idea/epa_mcp.iml +8 -0
- package/.idea/modules.xml +8 -0
- package/.idea/php.xml +19 -0
- package/.idea/vcs.xml +6 -0
- package/AGENTS.md +10 -0
- package/README.md +339 -0
- package/dist/agent/agentHelpers.js +21 -0
- package/dist/agent/openaiAgent.js +170 -0
- package/dist/agent/slidingWindow.js +13 -0
- package/dist/api/epaApiClient.js +59 -0
- package/dist/cli/index.js +47 -0
- package/dist/config/credentialStore.js +92 -0
- package/dist/config/ensureCliAuth.js +127 -0
- package/dist/config/loadConfig.js +40 -0
- package/dist/config/setup.js +23 -0
- package/dist/core/createReferenceTool.js +12 -0
- package/dist/core/createTool.js +32 -0
- package/dist/mocks/requestMocks.js +47 -0
- package/dist/server/server.js +41 -0
- package/dist/server/stdioServer.js +3 -0
- package/dist/services/request/requestFilters.js +1 -0
- package/dist/services/requestService.js +81 -0
- package/dist/services/teamService.js +18 -0
- package/dist/sql/createSqlConnection.js +13 -0
- package/dist/sql/fetchSchemaSummary.js +38 -0
- package/dist/sql/generateSqlFromQuestion.js +83 -0
- package/dist/sql/generateSqlPlan.js +111 -0
- package/dist/sql/getSqlAgentErrorMessage.js +15 -0
- package/dist/sql/loadSqlAgentConfig.js +24 -0
- package/dist/sql/loadSqlConfig.js +43 -0
- package/dist/sql/parseSqlQuestionHints.js +29 -0
- package/dist/sql/runSqlAgentCli.js +163 -0
- package/dist/sql/runSqlCli.js +34 -0
- package/dist/sql/selectRelevantTables.js +136 -0
- package/dist/sql/sqlGuard.js +21 -0
- package/dist/sql/sqlPlan.js +16 -0
- package/dist/tests/requestService.test.js +110 -0
- package/dist/tools/analytics/teamReport.draft.js +16 -0
- package/dist/tools/loadTools.js +40 -0
- package/dist/tools/requests/assignees.js +50 -0
- package/dist/tools/requests/clients.draft.js +10 -0
- package/dist/tools/requests/create.draft.js +51 -0
- package/dist/tools/requests/list.js +50 -0
- package/dist/tools/requests/priorities.js +2 -0
- package/dist/tools/requests/services.draft.js +20 -0
- package/dist/tools/requests/types.js +16 -0
- package/dist/tools/requests/units.draft.js +10 -0
- package/dist/tools/requests/view.draft.js +20 -0
- package/dist/utils/buildDateRange.js +18 -0
- package/dist/utils/findIdByDescription.js +4 -0
- package/dist/utils/resolveAssigneeId.js +14 -0
- package/dist/utils/toolNameMaps.js +23 -0
- package/package.json +31 -0
- package/src/agent/agentHelpers.ts +25 -0
- package/src/agent/openaiAgent.ts +205 -0
- package/src/agent/slidingWindow.ts +17 -0
- package/src/api/epaApiClient.ts +82 -0
- package/src/cli/index.ts +61 -0
- package/src/config/credentialStore.ts +130 -0
- package/src/config/ensureCliAuth.ts +152 -0
- package/src/config/loadConfig.ts +62 -0
- package/src/config/setup.ts +35 -0
- package/src/core/createReferenceTool.ts +17 -0
- package/src/core/createTool.ts +51 -0
- package/src/mocks/requestMocks.ts +52 -0
- package/src/server/server.ts +61 -0
- package/src/server/stdioServer.ts +5 -0
- package/src/services/request/requestFilters.ts +12 -0
- package/src/services/requestService.ts +126 -0
- package/src/services/teamService.ts +27 -0
- package/src/sql/createSqlConnection.ts +15 -0
- package/src/sql/fetchSchemaSummary.ts +64 -0
- package/src/sql/generateSqlFromQuestion.ts +105 -0
- package/src/sql/generateSqlPlan.ts +133 -0
- package/src/sql/getSqlAgentErrorMessage.ts +24 -0
- package/src/sql/loadSqlAgentConfig.ts +33 -0
- package/src/sql/loadSqlConfig.ts +75 -0
- package/src/sql/parseSqlQuestionHints.ts +46 -0
- package/src/sql/runSqlAgentCli.ts +204 -0
- package/src/sql/runSqlCli.ts +40 -0
- package/src/sql/selectRelevantTables.ts +184 -0
- package/src/sql/sqlGuard.ts +28 -0
- package/src/sql/sqlPlan.ts +28 -0
- package/src/tests/requestService.test.ts +152 -0
- package/src/tools/analytics/teamReport.draft.ts +25 -0
- package/src/tools/loadTools.ts +59 -0
- package/src/tools/requests/assignees.ts +59 -0
- package/src/tools/requests/clients.draft.ts +18 -0
- package/src/tools/requests/create.draft.ts +59 -0
- package/src/tools/requests/list.ts +57 -0
- package/src/tools/requests/priorities.ts +6 -0
- package/src/tools/requests/services.draft.ts +24 -0
- package/src/tools/requests/types.ts +18 -0
- package/src/tools/requests/units.draft.ts +18 -0
- package/src/tools/requests/view.draft.ts +27 -0
- package/src/utils/buildDateRange.ts +22 -0
- package/src/utils/findIdByDescription.ts +10 -0
- package/src/utils/resolveAssigneeId.ts +24 -0
- package/src/utils/toolNameMaps.ts +33 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import OpenAI from "openai"
|
|
2
|
+
import readline from "readline"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { fileURLToPath } from "url"
|
|
5
|
+
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
8
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
|
|
9
|
+
import { loadConfig } from "../config/loadConfig.js"
|
|
10
|
+
import { ensureCliAuth } from "../config/ensureCliAuth.js"
|
|
11
|
+
import { createMcpServer } from "../server/server.js"
|
|
12
|
+
import { applySlidingWindow } from "./slidingWindow.js"
|
|
13
|
+
import { buildToolNameMaps, getErrorMessage, isSpawnPermissionError } from "./agentHelpers.js"
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MAX_NON_SYSTEM_MESSAGES = 30
|
|
16
|
+
|
|
17
|
+
export async function startAgent() {
|
|
18
|
+
await ensureCliAuth()
|
|
19
|
+
|
|
20
|
+
const config = loadConfig()
|
|
21
|
+
const apiKey = process.env.OPENAI_API_KEY || config.openaiApiKey
|
|
22
|
+
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
throw new Error("OpenAI API key nao encontrada. Defina OPENAI_API_KEY ou configure via setup.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const openai = new OpenAI({ apiKey })
|
|
28
|
+
|
|
29
|
+
const currentFilePath = fileURLToPath(import.meta.url)
|
|
30
|
+
const isDevRuntime = currentFilePath.endsWith(".ts")
|
|
31
|
+
const cliEntry = path.resolve(
|
|
32
|
+
path.dirname(currentFilePath),
|
|
33
|
+
"..",
|
|
34
|
+
"cli",
|
|
35
|
+
isDevRuntime ? "index.ts" : "index.js"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const client = new Client(
|
|
39
|
+
{
|
|
40
|
+
name: "epa-agent",
|
|
41
|
+
version: "0.1.0"
|
|
42
|
+
},
|
|
43
|
+
{ capabilities: {} }
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const transport = new StdioClientTransport(
|
|
48
|
+
isDevRuntime
|
|
49
|
+
? {
|
|
50
|
+
command: process.execPath,
|
|
51
|
+
args: ["--import", "tsx", cliEntry, "server"]
|
|
52
|
+
}
|
|
53
|
+
: {
|
|
54
|
+
command: process.execPath,
|
|
55
|
+
args: [cliEntry, "server"]
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
await client.connect(transport)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!isSpawnPermissionError(error)) {
|
|
61
|
+
throw error
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
|
|
65
|
+
const server = await createMcpServer()
|
|
66
|
+
await server.connect(serverTransport)
|
|
67
|
+
await client.connect(clientTransport)
|
|
68
|
+
console.log("Aviso: modo in-memory ativado por restricao de spawn no ambiente.")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const toolsResponse = await client.listTools()
|
|
72
|
+
const toolNames = toolsResponse.tools.map((tool: any) => tool.name)
|
|
73
|
+
const { mcpToOpenAi, openAiToMcp } = buildToolNameMaps(toolNames)
|
|
74
|
+
|
|
75
|
+
const tools = toolsResponse.tools.map((tool: any) => ({
|
|
76
|
+
type: "function" as const,
|
|
77
|
+
function: {
|
|
78
|
+
name: mcpToOpenAi.get(tool.name) ?? tool.name,
|
|
79
|
+
description: tool.description,
|
|
80
|
+
parameters: tool.inputSchema
|
|
81
|
+
}
|
|
82
|
+
}))
|
|
83
|
+
|
|
84
|
+
const rl = readline.createInterface({
|
|
85
|
+
input: process.stdin,
|
|
86
|
+
output: process.stdout
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
console.log("EPA Agent iniciado")
|
|
90
|
+
|
|
91
|
+
const messages: any[] = [
|
|
92
|
+
{
|
|
93
|
+
role: "system",
|
|
94
|
+
content: `
|
|
95
|
+
Voce e um assistente que opera o sistema EPA.
|
|
96
|
+
|
|
97
|
+
Sempre utilize as ferramentas disponiveis quando necessario.
|
|
98
|
+
|
|
99
|
+
Fluxo recomendado para criar solicitacoes:
|
|
100
|
+
|
|
101
|
+
1. consultar requests.types
|
|
102
|
+
2. consultar requests.services
|
|
103
|
+
3. consultar requests.priorities/requests.assignees/requests.clients
|
|
104
|
+
4. executar requests.create
|
|
105
|
+
|
|
106
|
+
Para listar solicitacoes por usuario e periodo, prefira requests.list com:
|
|
107
|
+
- assignee_name (ex: suporte.simeon)
|
|
108
|
+
- period=last_12_months para "ultimo ano"
|
|
109
|
+
|
|
110
|
+
Regra para analise:
|
|
111
|
+
- Se voce ja recebeu uma lista de solicitacoes no contexto atual, faca a analise diretamente desses dados.
|
|
112
|
+
- Nao chame analytics.teamReport para analisar dados que ja estao no contexto.
|
|
113
|
+
- So use analytics.teamReport quando o usuario pedir explicitamente para buscar analise de um usuario especifico ou da equipe (ex: "analise da equipe", "relatorio do usuario X").
|
|
114
|
+
- Se analytics.teamReport nao for necessario, responda sem chamar tools.
|
|
115
|
+
`
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
const configuredMax = Number(process.env.AGENT_MAX_CONTEXT_MESSAGES)
|
|
119
|
+
const maxNonSystemMessages = Number.isFinite(configuredMax) && configuredMax > 0
|
|
120
|
+
? configuredMax
|
|
121
|
+
: DEFAULT_MAX_NON_SYSTEM_MESSAGES
|
|
122
|
+
|
|
123
|
+
rl.on("line", async (input) => {
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
messages.push({
|
|
127
|
+
role: "user",
|
|
128
|
+
content: input
|
|
129
|
+
})
|
|
130
|
+
applySlidingWindow(messages, maxNonSystemMessages)
|
|
131
|
+
|
|
132
|
+
let running = true
|
|
133
|
+
|
|
134
|
+
while (running) {
|
|
135
|
+
const completion = await openai.chat.completions.create({
|
|
136
|
+
model: "gpt-4o-mini",
|
|
137
|
+
messages,
|
|
138
|
+
tools,
|
|
139
|
+
tool_choice: "auto"
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const message = completion.choices[0].message
|
|
143
|
+
|
|
144
|
+
messages.push(message)
|
|
145
|
+
applySlidingWindow(messages, maxNonSystemMessages)
|
|
146
|
+
|
|
147
|
+
if (!message.tool_calls) {
|
|
148
|
+
console.log("\n[agent]", message.content, "\n")
|
|
149
|
+
running = false
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const toolCall of message.tool_calls) {
|
|
154
|
+
const openAiToolName = toolCall.function.name
|
|
155
|
+
const toolName = openAiToMcp.get(openAiToolName) ?? openAiToolName
|
|
156
|
+
|
|
157
|
+
let args: unknown
|
|
158
|
+
try {
|
|
159
|
+
args = JSON.parse(toolCall.function.arguments || "{}")
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const errorMessage = getErrorMessage(error)
|
|
162
|
+
console.error(`\n[erro] Argumentos invalidos para ${toolName}: ${errorMessage}\n`)
|
|
163
|
+
messages.push({
|
|
164
|
+
role: "tool",
|
|
165
|
+
tool_call_id: toolCall.id,
|
|
166
|
+
content: JSON.stringify([{ type: "text", text: `Erro: argumentos invalidos (${errorMessage})` }])
|
|
167
|
+
})
|
|
168
|
+
applySlidingWindow(messages, maxNonSystemMessages)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log(`\n[tool] Executando: ${toolName}`)
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const result = await client.callTool({
|
|
176
|
+
name: toolName,
|
|
177
|
+
arguments: args as Record<string, unknown>
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
messages.push({
|
|
181
|
+
role: "tool",
|
|
182
|
+
tool_call_id: toolCall.id,
|
|
183
|
+
content: JSON.stringify(result.content)
|
|
184
|
+
})
|
|
185
|
+
applySlidingWindow(messages, maxNonSystemMessages)
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const errorMessage = getErrorMessage(error)
|
|
188
|
+
console.error(`\n[erro] Falha ao executar ${toolName}: ${errorMessage}\n`)
|
|
189
|
+
messages.push({
|
|
190
|
+
role: "tool",
|
|
191
|
+
tool_call_id: toolCall.id,
|
|
192
|
+
content: JSON.stringify([{ type: "text", text: `Erro ao executar tool: ${errorMessage}` }])
|
|
193
|
+
})
|
|
194
|
+
applySlidingWindow(messages, maxNonSystemMessages)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
const errorMessage = getErrorMessage(error)
|
|
200
|
+
console.error(`\n[erro] Falha no processamento da mensagem: ${errorMessage}\n`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function applySlidingWindow(messages: any[], maxNonSystemMessages: number) {
|
|
2
|
+
if (!Number.isFinite(maxNonSystemMessages) || maxNonSystemMessages < 1) {
|
|
3
|
+
return
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const systemMessage = messages.find((message) => message?.role === "system")
|
|
7
|
+
const nonSystemMessages = messages.filter((message) => message?.role !== "system")
|
|
8
|
+
const trimmedNonSystem = nonSystemMessages.slice(-maxNonSystemMessages)
|
|
9
|
+
|
|
10
|
+
messages.length = 0
|
|
11
|
+
|
|
12
|
+
if (systemMessage) {
|
|
13
|
+
messages.push(systemMessage)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
messages.push(...trimmedNonSystem)
|
|
17
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from "axios"
|
|
2
|
+
import { loadConfig } from "../config/loadConfig.js"
|
|
3
|
+
|
|
4
|
+
export class EpaApiClient {
|
|
5
|
+
|
|
6
|
+
private client: AxiosInstance
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
|
|
10
|
+
const config = loadConfig()
|
|
11
|
+
|
|
12
|
+
const timeoutMs = Number(
|
|
13
|
+
config.epaApiTimeoutMs ??
|
|
14
|
+
process.env.EPA_API_TIMEOUT_MS ??
|
|
15
|
+
15000
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
this.client = axios.create({
|
|
19
|
+
baseURL: config.epaApiUrl,
|
|
20
|
+
timeout: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 15000,
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${config.epaApiToken}`
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async get(endpoint: string, params?: any) {
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const response = await this.client.get(endpoint, { params })
|
|
32
|
+
return response.data
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw this.formatError(error, "GET", endpoint)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async post(endpoint: string, body: any = {}, params?: any) {
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await this.client.post(endpoint, body, { params })
|
|
43
|
+
return response.data
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw this.formatError(error, "POST", endpoint)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private formatError(error: unknown, method: string, endpoint: string): Error {
|
|
51
|
+
|
|
52
|
+
if (axios.isAxiosError(error)) {
|
|
53
|
+
const status = error.response?.status
|
|
54
|
+
const apiMessage = this.extractApiMessage(error.response?.data)
|
|
55
|
+
const suffix = status ? `status ${status}` : "sem status HTTP"
|
|
56
|
+
return new Error(`[EPA API] ${method} ${endpoint} falhou (${suffix}): ${apiMessage}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
60
|
+
return new Error(`[EPA API] ${method} ${endpoint} falhou: ${message}`)
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private extractApiMessage(data: unknown): string {
|
|
65
|
+
|
|
66
|
+
if (typeof data === "string" && data.trim()) {
|
|
67
|
+
return data
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (data && typeof data === "object") {
|
|
71
|
+
const maybeMessage = (data as any).message ?? (data as any).error
|
|
72
|
+
if (typeof maybeMessage === "string" && maybeMessage.trim()) {
|
|
73
|
+
return maybeMessage
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify(data)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return "erro sem detalhes retornados pela API"
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { startServer } from "../server/server.js";
|
|
5
|
+
import { setupConfig } from "../config/setup.js";
|
|
6
|
+
import { ensureCliAuth } from "../config/ensureCliAuth.js";
|
|
7
|
+
import { startAgent } from "../agent/openaiAgent.js"
|
|
8
|
+
import { runSqlCli } from "../sql/runSqlCli.js";
|
|
9
|
+
import { runSqlAgentCli } from "../sql/runSqlAgentCli.js";
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command("setup")
|
|
15
|
+
.description("Configurar URL da API do EPA")
|
|
16
|
+
.action(async () => {
|
|
17
|
+
await setupConfig();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("login")
|
|
22
|
+
.description("Realiza login na API do EPA e salva token para CLI")
|
|
23
|
+
.action(async () => {
|
|
24
|
+
await ensureCliAuth();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command("server")
|
|
29
|
+
.description("Iniciar MCP Server")
|
|
30
|
+
.action(async () => {
|
|
31
|
+
|
|
32
|
+
console.error("Iniciando EPA MCP Server...");
|
|
33
|
+
|
|
34
|
+
await startServer();
|
|
35
|
+
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command("agent")
|
|
40
|
+
.description("Inicia agente OpenAI conectado ao MCP")
|
|
41
|
+
.action(async () => {
|
|
42
|
+
|
|
43
|
+
await startAgent()
|
|
44
|
+
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program
|
|
48
|
+
.command("sql [query]")
|
|
49
|
+
.description("Executa consulta SQL técnica read-only no MySQL/MariaDB")
|
|
50
|
+
.action(async (query?: string) => {
|
|
51
|
+
await runSqlCli(query)
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command("sql-agent [question]")
|
|
56
|
+
.description("Usa LLM para gerar e executar SQL read-only no MySQL/MariaDB")
|
|
57
|
+
.action(async (question?: string) => {
|
|
58
|
+
await runSqlAgentCli(question)
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
program.parse();
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
type ConfigJson = {
|
|
5
|
+
epaApiUrl?: string
|
|
6
|
+
epaApiToken?: string
|
|
7
|
+
epaApiTimeoutMs?: number
|
|
8
|
+
openaiApiKey?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseEnvFile(raw: string): Record<string, string> {
|
|
12
|
+
const result: Record<string, string> = {}
|
|
13
|
+
const lines = raw.split(/\r?\n/)
|
|
14
|
+
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const trimmed = line.trim()
|
|
17
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
18
|
+
continue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const index = trimmed.indexOf("=")
|
|
22
|
+
if (index < 0) {
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const key = trimmed.slice(0, index).trim()
|
|
27
|
+
const value = trimmed.slice(index + 1).trim()
|
|
28
|
+
result[key] = value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stringifyEnv(values: Record<string, string>): string {
|
|
35
|
+
return Object.entries(values)
|
|
36
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
37
|
+
.join("\n")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveCredentialStorePath(): { kind: "env" | "config"; path: string } {
|
|
41
|
+
const envPath = path.join(process.cwd(), ".env")
|
|
42
|
+
if (fs.existsSync(envPath)) {
|
|
43
|
+
return { kind: "env", path: envPath }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const configPath = path.join(process.cwd(), "config", "config.json")
|
|
47
|
+
return { kind: "config", path: configPath }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readStoredCredentials(): {
|
|
51
|
+
epaApiUrl?: string
|
|
52
|
+
epaApiToken?: string
|
|
53
|
+
openaiApiKey?: string
|
|
54
|
+
epaApiTimeoutMs?: number
|
|
55
|
+
} {
|
|
56
|
+
const store = resolveCredentialStorePath()
|
|
57
|
+
|
|
58
|
+
if (store.kind === "env") {
|
|
59
|
+
const raw = fs.readFileSync(store.path, "utf-8")
|
|
60
|
+
const values = parseEnvFile(raw)
|
|
61
|
+
const timeout = Number(values.EPA_API_TIMEOUT_MS ?? values.epaApiTimeoutMs)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
epaApiUrl: values.EPA_API_URL ?? values.epaApiUrl,
|
|
65
|
+
epaApiToken: values.EPA_API_TOKEN ?? values.epaApiToken,
|
|
66
|
+
openaiApiKey: values.OPENAI_API_KEY ?? values.openaiApiKey,
|
|
67
|
+
epaApiTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!fs.existsSync(store.path)) {
|
|
72
|
+
return {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const raw = fs.readFileSync(store.path, "utf-8")
|
|
76
|
+
const json = JSON.parse(raw) as ConfigJson
|
|
77
|
+
return {
|
|
78
|
+
epaApiUrl: json.epaApiUrl,
|
|
79
|
+
epaApiToken: json.epaApiToken,
|
|
80
|
+
openaiApiKey: json.openaiApiKey,
|
|
81
|
+
epaApiTimeoutMs: json.epaApiTimeoutMs
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function writeCredentials(values: {
|
|
86
|
+
epaApiUrl?: string
|
|
87
|
+
epaApiToken?: string
|
|
88
|
+
openaiApiKey?: string
|
|
89
|
+
epaApiTimeoutMs?: number
|
|
90
|
+
}) {
|
|
91
|
+
const store = resolveCredentialStorePath()
|
|
92
|
+
|
|
93
|
+
if (store.kind === "env") {
|
|
94
|
+
const existing = fs.existsSync(store.path)
|
|
95
|
+
? parseEnvFile(fs.readFileSync(store.path, "utf-8"))
|
|
96
|
+
: {}
|
|
97
|
+
|
|
98
|
+
if (values.epaApiUrl) {
|
|
99
|
+
existing.EPA_API_URL = values.epaApiUrl
|
|
100
|
+
}
|
|
101
|
+
if (values.epaApiToken) {
|
|
102
|
+
existing.EPA_API_TOKEN = values.epaApiToken
|
|
103
|
+
}
|
|
104
|
+
if (values.openaiApiKey) {
|
|
105
|
+
existing.OPENAI_API_KEY = values.openaiApiKey
|
|
106
|
+
}
|
|
107
|
+
if (values.epaApiTimeoutMs !== undefined) {
|
|
108
|
+
existing.EPA_API_TIMEOUT_MS = String(values.epaApiTimeoutMs)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fs.writeFileSync(store.path, `${stringifyEnv(existing)}\n`)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const configDir = path.dirname(store.path)
|
|
116
|
+
if (!fs.existsSync(configDir)) {
|
|
117
|
+
fs.mkdirSync(configDir)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const current = fs.existsSync(store.path)
|
|
121
|
+
? (JSON.parse(fs.readFileSync(store.path, "utf-8")) as ConfigJson)
|
|
122
|
+
: {}
|
|
123
|
+
|
|
124
|
+
const next: ConfigJson = {
|
|
125
|
+
...current,
|
|
126
|
+
...values
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fs.writeFileSync(store.path, JSON.stringify(next, null, 2))
|
|
130
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import axios from "axios"
|
|
2
|
+
import readline from "readline"
|
|
3
|
+
import { readStoredCredentials, writeCredentials } from "./credentialStore.js"
|
|
4
|
+
|
|
5
|
+
function askQuestion(rl: readline.Interface, question: string): Promise<string> {
|
|
6
|
+
return new Promise((resolve) => rl.question(question, resolve))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function askHiddenQuestion(question: string): Promise<string> {
|
|
10
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
11
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question(question, (answer) => {
|
|
14
|
+
rl.close()
|
|
15
|
+
resolve(answer)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
let value = ""
|
|
22
|
+
const stdin = process.stdin
|
|
23
|
+
const stdout = process.stdout
|
|
24
|
+
const canSetRawMode = typeof stdin.setRawMode === "function"
|
|
25
|
+
|
|
26
|
+
stdout.write(question)
|
|
27
|
+
stdout.write("\x1B[8m")
|
|
28
|
+
stdin.setEncoding("utf8")
|
|
29
|
+
if (canSetRawMode) {
|
|
30
|
+
stdin.setRawMode(true)
|
|
31
|
+
}
|
|
32
|
+
stdin.resume()
|
|
33
|
+
|
|
34
|
+
const onData = (char: string) => {
|
|
35
|
+
|
|
36
|
+
if (char === "\u0003") {
|
|
37
|
+
cleanup()
|
|
38
|
+
reject(new Error("Entrada cancelada pelo usuário."))
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (char === "\r" || char === "\n") {
|
|
43
|
+
stdout.write("\n")
|
|
44
|
+
cleanup()
|
|
45
|
+
resolve(value)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (char === "\u0008" || char === "\u007F") {
|
|
50
|
+
if (value.length > 0) {
|
|
51
|
+
value = value.slice(0, -1)
|
|
52
|
+
stdout.write("\b \b")
|
|
53
|
+
}
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sanitized = char.replace(/[\r\n]/g, "")
|
|
58
|
+
if (!sanitized) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
value += sanitized
|
|
63
|
+
stdout.write("*".repeat(sanitized.length))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cleanup = () => {
|
|
67
|
+
stdin.off("data", onData)
|
|
68
|
+
if (canSetRawMode) {
|
|
69
|
+
stdin.setRawMode(false)
|
|
70
|
+
}
|
|
71
|
+
stdout.write("\x1B[28m")
|
|
72
|
+
stdin.pause()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
stdin.on("data", onData)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function isTokenValid(baseURL: string, token: string): Promise<boolean> {
|
|
80
|
+
try {
|
|
81
|
+
await axios.get(`${baseURL}/api/api/prioridade`, {
|
|
82
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
83
|
+
timeout: 15000
|
|
84
|
+
})
|
|
85
|
+
return true
|
|
86
|
+
} catch {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loginToEpa(baseURL: string, login: string, senha: string): Promise<string> {
|
|
92
|
+
const response = await axios.post(
|
|
93
|
+
`${baseURL}/api/api/novo/login`,
|
|
94
|
+
{ login, senha },
|
|
95
|
+
{ timeout: 15000 }
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const token = response?.data?.access_token
|
|
99
|
+
if (!token || typeof token !== "string") {
|
|
100
|
+
throw new Error("Login realizado, mas access_token nao foi retornado pela API.")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return token
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function ensureCliAuth() {
|
|
107
|
+
const stored = readStoredCredentials()
|
|
108
|
+
const rl = readline.createInterface({
|
|
109
|
+
input: process.stdin,
|
|
110
|
+
output: process.stdout
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
let epaApiUrl = process.env.EPA_API_URL || process.env.epaApiUrl || stored.epaApiUrl
|
|
115
|
+
let epaApiToken = process.env.EPA_API_TOKEN || process.env.epaApiToken || stored.epaApiToken
|
|
116
|
+
const openaiApiKey = process.env.OPENAI_API_KEY || process.env.openaiApiKey || stored.openaiApiKey
|
|
117
|
+
const epaApiTimeoutMs = stored.epaApiTimeoutMs ?? 15000
|
|
118
|
+
|
|
119
|
+
if (!epaApiUrl) {
|
|
120
|
+
epaApiUrl = (await askQuestion(rl, "EPA API URL: ")).trim()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let tokenValid = false
|
|
124
|
+
if (epaApiUrl && epaApiToken) {
|
|
125
|
+
tokenValid = await isTokenValid(epaApiUrl, epaApiToken)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!epaApiToken || !tokenValid) {
|
|
129
|
+
console.log("Token EPA ausente ou invalido. Realizando login...")
|
|
130
|
+
const login = (await askQuestion(rl, "Login EPA: ")).trim()
|
|
131
|
+
rl.pause()
|
|
132
|
+
const senha = (await askHiddenQuestion("Senha EPA: ")).trim()
|
|
133
|
+
rl.resume()
|
|
134
|
+
epaApiToken = await loginToEpa(epaApiUrl, login, senha)
|
|
135
|
+
console.log("Login realizado com sucesso.")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let nextOpenAi = openaiApiKey
|
|
139
|
+
if (!nextOpenAi) {
|
|
140
|
+
nextOpenAi = (await askQuestion(rl, "OpenAI API KEY (CLI): ")).trim()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
writeCredentials({
|
|
144
|
+
epaApiUrl,
|
|
145
|
+
epaApiToken,
|
|
146
|
+
openaiApiKey: nextOpenAi || undefined,
|
|
147
|
+
epaApiTimeoutMs
|
|
148
|
+
})
|
|
149
|
+
} finally {
|
|
150
|
+
rl.close()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import dotenv from "dotenv"
|
|
4
|
+
|
|
5
|
+
dotenv.config()
|
|
6
|
+
|
|
7
|
+
export interface AppConfig {
|
|
8
|
+
epaApiUrl: string
|
|
9
|
+
epaApiToken: string
|
|
10
|
+
openaiApiKey?: string
|
|
11
|
+
epaApiTimeoutMs?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readConfigFile(): Partial<AppConfig> {
|
|
15
|
+
const configPath = path.join(process.cwd(), "config", "config.json")
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(configPath)) {
|
|
18
|
+
return {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const raw = fs.readFileSync(configPath, "utf-8")
|
|
22
|
+
return JSON.parse(raw) as Partial<AppConfig>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getEnvValue(...keys: string[]): string | undefined {
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
const value = process.env[key]
|
|
28
|
+
if (value && value.trim()) {
|
|
29
|
+
return value.trim()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadConfig(): AppConfig {
|
|
37
|
+
const fileConfig = readConfigFile()
|
|
38
|
+
|
|
39
|
+
const epaApiUrl = getEnvValue("EPA_API_URL", "epaApiUrl") ?? fileConfig.epaApiUrl
|
|
40
|
+
const epaApiToken = getEnvValue("EPA_API_TOKEN", "epaApiToken") ?? fileConfig.epaApiToken
|
|
41
|
+
const openaiApiKey = getEnvValue("OPENAI_API_KEY", "openaiApiKey") ?? fileConfig.openaiApiKey
|
|
42
|
+
|
|
43
|
+
const timeoutRaw =
|
|
44
|
+
getEnvValue("EPA_API_TIMEOUT_MS", "epaApiTimeoutMs") ??
|
|
45
|
+
(fileConfig.epaApiTimeoutMs !== undefined ? String(fileConfig.epaApiTimeoutMs) : undefined)
|
|
46
|
+
|
|
47
|
+
const timeout = timeoutRaw ? Number(timeoutRaw) : undefined
|
|
48
|
+
const epaApiTimeoutMs = Number.isFinite(timeout) && Number(timeout) > 0 ? Number(timeout) : undefined
|
|
49
|
+
|
|
50
|
+
if (!epaApiUrl || !epaApiToken) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'Configuracao incompleta. Defina "EPA_API_URL" e "EPA_API_TOKEN" nas variaveis de ambiente ou em config/config.json.'
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
epaApiUrl,
|
|
58
|
+
epaApiToken,
|
|
59
|
+
openaiApiKey,
|
|
60
|
+
epaApiTimeoutMs
|
|
61
|
+
}
|
|
62
|
+
}
|